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,78 @@
|
|
1
|
+
# Shamelessly stolen from Mongoid!
|
2
|
+
module Dynamoid #:nodoc
|
3
|
+
module Config
|
4
|
+
|
5
|
+
# Encapsulates logic for setting options.
|
6
|
+
module Options
|
7
|
+
|
8
|
+
# Get the defaults or initialize a new empty hash.
|
9
|
+
#
|
10
|
+
# @example Get the defaults.
|
11
|
+
# options.defaults
|
12
|
+
#
|
13
|
+
# @return [ Hash ] The default options.
|
14
|
+
#
|
15
|
+
# @since 0.2.0
|
16
|
+
def defaults
|
17
|
+
@defaults ||= {}
|
18
|
+
end
|
19
|
+
|
20
|
+
# Define a configuration option with a default.
|
21
|
+
#
|
22
|
+
# @example Define the option.
|
23
|
+
# Options.option(:persist_in_safe_mode, :default => false)
|
24
|
+
#
|
25
|
+
# @param [ Symbol ] name The name of the configuration option.
|
26
|
+
# @param [ Hash ] options Extras for the option.
|
27
|
+
#
|
28
|
+
# @option options [ Object ] :default The default value.
|
29
|
+
#
|
30
|
+
# @since 0.2.0
|
31
|
+
def option(name, options = {})
|
32
|
+
defaults[name] = settings[name] = options[:default]
|
33
|
+
|
34
|
+
class_eval <<-RUBY
|
35
|
+
def #{name}
|
36
|
+
settings[#{name.inspect}]
|
37
|
+
end
|
38
|
+
|
39
|
+
def #{name}=(value)
|
40
|
+
settings[#{name.inspect}] = value
|
41
|
+
end
|
42
|
+
|
43
|
+
def #{name}?
|
44
|
+
#{name}
|
45
|
+
end
|
46
|
+
|
47
|
+
def reset_#{name}
|
48
|
+
settings[#{name.inspect}] = defaults[#{name.inspect}]
|
49
|
+
end
|
50
|
+
RUBY
|
51
|
+
end
|
52
|
+
|
53
|
+
# Reset the configuration options to the defaults.
|
54
|
+
#
|
55
|
+
# @example Reset the configuration options.
|
56
|
+
# config.reset
|
57
|
+
#
|
58
|
+
# @return [ Hash ] The defaults.
|
59
|
+
#
|
60
|
+
# @since 0.2.0
|
61
|
+
def reset
|
62
|
+
settings.replace(defaults)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Get the settings or initialize a new empty hash.
|
66
|
+
#
|
67
|
+
# @example Get the settings.
|
68
|
+
# options.settings
|
69
|
+
#
|
70
|
+
# @return [ Hash ] The setting options.
|
71
|
+
#
|
72
|
+
# @since 0.2.0
|
73
|
+
def settings
|
74
|
+
@settings ||= {}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "uri"
|
3
|
+
require "dynamoid/config/options"
|
4
|
+
|
5
|
+
module Dynamoid
|
6
|
+
|
7
|
+
# Contains all the basic configuration information required for Dynamoid: both sensible defaults and required fields.
|
8
|
+
module Config
|
9
|
+
extend self
|
10
|
+
extend Options
|
11
|
+
include ActiveModel::Observing if defined?(ActiveModel::Observing)
|
12
|
+
|
13
|
+
# All the default options.
|
14
|
+
option :adapter, :default => 'aws_sdk_v2'
|
15
|
+
option :namespace, :default => defined?(Rails) ? "dynamoid_#{Rails.application.class.parent_name}_#{Rails.env}" : "dynamoid"
|
16
|
+
option :logger, :default => defined?(Rails)
|
17
|
+
option :access_key
|
18
|
+
option :secret_key
|
19
|
+
option :read_capacity, :default => 100
|
20
|
+
option :write_capacity, :default => 20
|
21
|
+
option :warn_on_scan, :default => true
|
22
|
+
option :endpoint, :default => nil
|
23
|
+
option :use_ssl, :default => true
|
24
|
+
option :port, :default => '443'
|
25
|
+
option :identity_map, :default => false
|
26
|
+
|
27
|
+
# The default logger for Dynamoid: either the Rails logger or just stdout.
|
28
|
+
#
|
29
|
+
# @since 0.2.0
|
30
|
+
def default_logger
|
31
|
+
defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : ::Logger.new($stdout)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns the assigned logger instance.
|
35
|
+
#
|
36
|
+
# @since 0.2.0
|
37
|
+
def logger
|
38
|
+
@logger ||= default_logger
|
39
|
+
end
|
40
|
+
|
41
|
+
# If you want to, set the logger manually to any output you'd like. Or pass false or nil to disable logging entirely.
|
42
|
+
#
|
43
|
+
# @since 0.2.0
|
44
|
+
def logger=(logger)
|
45
|
+
case logger
|
46
|
+
when false, nil then @logger = nil
|
47
|
+
when true then @logger = default_logger
|
48
|
+
else
|
49
|
+
@logger = logger if logger.respond_to?(:info)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Dynamoid #:nodoc:
|
3
|
+
module Criteria
|
4
|
+
|
5
|
+
# The criteria chain is equivalent to an ActiveRecord relation (and realistically I should change the name from
|
6
|
+
# chain to relation). It is a chainable object that builds up a query and eventually executes it by a Query or Scan.
|
7
|
+
class Chain
|
8
|
+
attr_accessor :query, :source, :values, :consistent_read
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
# Create a new criteria chain.
|
12
|
+
#
|
13
|
+
# @param [Class] source the class upon which the ultimate query will be performed.
|
14
|
+
def initialize(source)
|
15
|
+
@query = {}
|
16
|
+
@source = source
|
17
|
+
@consistent_read = false
|
18
|
+
@scan_index_forward = true
|
19
|
+
end
|
20
|
+
|
21
|
+
# The workhorse method of the criteria chain. Each key in the passed in hash will become another criteria that the
|
22
|
+
# ultimate query must match. A key can either be a symbol or a string, and should be an attribute name or
|
23
|
+
# an attribute name with a range operator.
|
24
|
+
#
|
25
|
+
# @example A simple criteria
|
26
|
+
# where(:name => 'Josh')
|
27
|
+
#
|
28
|
+
# @example A more complicated criteria
|
29
|
+
# where(:name => 'Josh', 'created_at.gt' => DateTime.now - 1.day)
|
30
|
+
#
|
31
|
+
# @since 0.2.0
|
32
|
+
def where(args)
|
33
|
+
args.each {|k, v| query[k.to_sym] = v}
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
def consistent
|
38
|
+
@consistent_read = true
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns all the records matching the criteria.
|
43
|
+
#
|
44
|
+
# @since 0.2.0
|
45
|
+
def all
|
46
|
+
records
|
47
|
+
end
|
48
|
+
|
49
|
+
# Destroys all the records matching the criteria.
|
50
|
+
#
|
51
|
+
def destroy_all
|
52
|
+
ids = []
|
53
|
+
|
54
|
+
if key_present?
|
55
|
+
ranges = []
|
56
|
+
Dynamoid.adapter.query(source.table_name, range_query).collect do |hash|
|
57
|
+
ids << hash[source.hash_key.to_sym]
|
58
|
+
ranges << hash[source.range_key.to_sym]
|
59
|
+
end
|
60
|
+
|
61
|
+
Dynamoid.adapter.delete(source.table_name, ids,{:range_key => ranges})
|
62
|
+
else
|
63
|
+
Dynamoid.adapter.scan(source.table_name, query, scan_opts).collect do |hash|
|
64
|
+
ids << hash[source.hash_key.to_sym]
|
65
|
+
end
|
66
|
+
|
67
|
+
Dynamoid.adapter.delete(source.table_name, ids)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def eval_limit(limit)
|
72
|
+
@eval_limit = limit
|
73
|
+
self
|
74
|
+
end
|
75
|
+
|
76
|
+
def batch(batch_size)
|
77
|
+
@batch_size = batch_size
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
def start(start)
|
82
|
+
@start = start
|
83
|
+
self
|
84
|
+
end
|
85
|
+
|
86
|
+
def scan_index_forward(scan_index_forward)
|
87
|
+
@scan_index_forward = scan_index_forward
|
88
|
+
self
|
89
|
+
end
|
90
|
+
|
91
|
+
# Allows you to use the results of a search as an enumerable over the results found.
|
92
|
+
#
|
93
|
+
# @since 0.2.0
|
94
|
+
def each(&block)
|
95
|
+
records.each(&block)
|
96
|
+
end
|
97
|
+
|
98
|
+
def consistent_opts
|
99
|
+
{ :consistent_read => consistent_read }
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
# The actual records referenced by the association.
|
105
|
+
#
|
106
|
+
# @return [Enumerator] an iterator of the found records.
|
107
|
+
#
|
108
|
+
# @since 0.2.0
|
109
|
+
def records
|
110
|
+
results = if key_present?
|
111
|
+
records_via_query
|
112
|
+
else
|
113
|
+
records_via_scan
|
114
|
+
end
|
115
|
+
@batch_size ? results : Array(results)
|
116
|
+
end
|
117
|
+
|
118
|
+
def records_via_query
|
119
|
+
Enumerator.new do |yielder|
|
120
|
+
Dynamoid.adapter.query(source.table_name, range_query).each do |hash|
|
121
|
+
yielder.yield source.from_database(hash)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# If the query does not match an index, we'll manually scan the associated table to find results.
|
127
|
+
#
|
128
|
+
# @return [Enumerator] an iterator of the found records.
|
129
|
+
#
|
130
|
+
# @since 0.2.0
|
131
|
+
def records_via_scan
|
132
|
+
if Dynamoid::Config.warn_on_scan
|
133
|
+
Dynamoid.logger.warn 'Queries without an index are forced to use scan and are generally much slower than indexed queries!'
|
134
|
+
Dynamoid.logger.warn "You can index this query by adding this to #{source.to_s.downcase}.rb: index [#{source.attributes.sort.collect{|attr| ":#{attr}"}.join(', ')}]"
|
135
|
+
end
|
136
|
+
|
137
|
+
if @consistent_read
|
138
|
+
raise Dynamoid::Errors::InvalidQuery, 'Consistent read is not supported by SCAN operation'
|
139
|
+
end
|
140
|
+
|
141
|
+
Enumerator.new do |yielder|
|
142
|
+
Dynamoid.adapter.scan(source.table_name, query, scan_opts).each do |hash|
|
143
|
+
yielder.yield source.from_database(hash)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def range_hash(key)
|
149
|
+
val = query[key]
|
150
|
+
|
151
|
+
return { :range_value => query[key] } if query[key].is_a?(Range)
|
152
|
+
|
153
|
+
case key.to_s.split('.').last
|
154
|
+
when 'gt'
|
155
|
+
{ :range_greater_than => val.to_f }
|
156
|
+
when 'lt'
|
157
|
+
{ :range_less_than => val.to_f }
|
158
|
+
when 'gte'
|
159
|
+
{ :range_gte => val.to_f }
|
160
|
+
when 'lte'
|
161
|
+
{ :range_lte => val.to_f }
|
162
|
+
when 'begins_with'
|
163
|
+
{ :range_begins_with => val }
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def range_query
|
168
|
+
opts = { :hash_value => query[source.hash_key] }
|
169
|
+
if key = query.keys.find { |k| k.to_s.include?('.') }
|
170
|
+
opts.merge!(range_hash(key))
|
171
|
+
end
|
172
|
+
opts.merge(query_opts).merge(consistent_opts)
|
173
|
+
end
|
174
|
+
|
175
|
+
def query_keys
|
176
|
+
query.keys.collect{|k| k.to_s.split('.').first}
|
177
|
+
end
|
178
|
+
|
179
|
+
# [hash_key] or [hash_key, range_key] is specified in query keys.
|
180
|
+
def key_present?
|
181
|
+
query_keys == [source.hash_key.to_s] || (query_keys.to_set == [source.hash_key.to_s, source.range_key.to_s].to_set)
|
182
|
+
end
|
183
|
+
|
184
|
+
def start_key
|
185
|
+
key = { :hash_key_element => @start.hash_key }
|
186
|
+
if range_key = @start.class.range_key
|
187
|
+
key.merge!({:range_key_element => @start.send(range_key) })
|
188
|
+
end
|
189
|
+
key
|
190
|
+
end
|
191
|
+
|
192
|
+
def query_opts
|
193
|
+
opts = {}
|
194
|
+
opts[:select] = 'ALL_ATTRIBUTES'
|
195
|
+
opts[:limit] = @eval_limit if @eval_limit
|
196
|
+
opts[:next_token] = start_key if @start
|
197
|
+
opts[:scan_index_forward] = @scan_index_forward
|
198
|
+
opts
|
199
|
+
end
|
200
|
+
|
201
|
+
def scan_opts
|
202
|
+
opts = {}
|
203
|
+
opts[:limit] = @eval_limit if @eval_limit
|
204
|
+
opts[:next_token] = start_key if @start
|
205
|
+
opts[:batch_size] = @batch_size if @batch_size
|
206
|
+
opts
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
end
|
211
|
+
|
212
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'dynamoid/criteria/chain'
|
3
|
+
|
4
|
+
module Dynamoid
|
5
|
+
|
6
|
+
# Allows classes to be queried by where, all, first, and each and return criteria chains.
|
7
|
+
module Criteria
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
[:where, :all, :first, :each, :eval_limit, :start, :scan_index_forward].each do |meth|
|
13
|
+
# Return a criteria chain in response to a method that will begin or end a chain. For more information,
|
14
|
+
# see Dynamoid::Criteria::Chain.
|
15
|
+
#
|
16
|
+
# @since 0.2.0
|
17
|
+
define_method(meth) do |*args|
|
18
|
+
chain = Dynamoid::Criteria::Chain.new(self)
|
19
|
+
if args
|
20
|
+
chain.send(meth, *args)
|
21
|
+
else
|
22
|
+
chain.send(meth)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -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
|
+
attrs[:type] ? attrs[:type].constantize.new(attrs).tap(&:save) : new(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
|
+
attrs[:type] ? attrs[:type].constantize.new(attrs).tap(&:save!) : new(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,63 @@
|
|
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
|
+
def initialize(document)
|
57
|
+
super("Validation failed: #{document.errors.full_messages.join(", ")}")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class InvalidQuery < Error; end
|
62
|
+
end
|
63
|
+
end
|