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,69 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Dynamoid #:nodoc:
|
3
|
+
|
4
|
+
module Associations
|
5
|
+
module SingleAssociation
|
6
|
+
include Association
|
7
|
+
|
8
|
+
delegate :class, :to => :target
|
9
|
+
|
10
|
+
def setter(object)
|
11
|
+
delete
|
12
|
+
source.update_attribute(source_attribute, Set[object.hash_key])
|
13
|
+
self.send(:associate_target, object) if target_association
|
14
|
+
object
|
15
|
+
end
|
16
|
+
|
17
|
+
def delete
|
18
|
+
source.update_attribute(source_attribute, nil)
|
19
|
+
self.send(:disassociate_target, target) if target && target_association
|
20
|
+
target
|
21
|
+
end
|
22
|
+
|
23
|
+
def create!(attributes = {})
|
24
|
+
setter(target_class.create!(attributes))
|
25
|
+
end
|
26
|
+
|
27
|
+
def create(attributes = {})
|
28
|
+
setter(target_class.create(attributes))
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
# Is this object equal to the association's target?
|
33
|
+
#
|
34
|
+
# @return [Boolean] true/false
|
35
|
+
#
|
36
|
+
# @since 0.2.0
|
37
|
+
def ==(other)
|
38
|
+
target == other
|
39
|
+
end
|
40
|
+
|
41
|
+
# Delegate methods we don't find directly to the target.
|
42
|
+
#
|
43
|
+
# @since 0.2.0
|
44
|
+
def method_missing(method, *args)
|
45
|
+
if target.respond_to?(method)
|
46
|
+
target.send(method, *args)
|
47
|
+
else
|
48
|
+
super
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def nil?
|
53
|
+
target.nil?
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# Find the target of the has_one association.
|
59
|
+
#
|
60
|
+
# @return [Dynamoid::Document] the found target (or nil if nothing)
|
61
|
+
#
|
62
|
+
# @since 0.2.0
|
63
|
+
def find_target
|
64
|
+
return if source_ids.empty?
|
65
|
+
target_class.find(source_ids.first)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Dynamoid
|
3
|
+
|
4
|
+
# All modules that a Document is composed of are defined in this
|
5
|
+
# module, to keep the document class from getting too cluttered.
|
6
|
+
module Components
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
extend ActiveModel::Translation
|
11
|
+
extend ActiveModel::Callbacks
|
12
|
+
|
13
|
+
define_model_callbacks :create, :save, :destroy, :initialize, :update
|
14
|
+
|
15
|
+
before_create :set_created_at
|
16
|
+
before_save :set_updated_at
|
17
|
+
after_initialize :set_type
|
18
|
+
end
|
19
|
+
|
20
|
+
include ActiveModel::AttributeMethods
|
21
|
+
include ActiveModel::Conversion
|
22
|
+
include ActiveModel::MassAssignmentSecurity if defined?(ActiveModel::MassAssignmentSecurity)
|
23
|
+
include ActiveModel::Naming
|
24
|
+
include ActiveModel::Observing if defined?(ActiveModel::Observing)
|
25
|
+
include ActiveModel::Serializers::JSON
|
26
|
+
include ActiveModel::Serializers::Xml
|
27
|
+
include Dynamoid::Fields
|
28
|
+
include Dynamoid::Indexes
|
29
|
+
include Dynamoid::Persistence
|
30
|
+
include Dynamoid::Finders
|
31
|
+
include Dynamoid::Associations
|
32
|
+
include Dynamoid::Criteria
|
33
|
+
include Dynamoid::Validations
|
34
|
+
include Dynamoid::IdentityMap
|
35
|
+
include Dynamoid::Dirty
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,58 @@
|
|
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
|
+
option :timestamps, :default => true
|
27
|
+
option :sync_retry_max_times, :default => 60 # a bit over 2 minutes
|
28
|
+
option :sync_retry_wait_seconds, :default => 2
|
29
|
+
option :convert_big_decimal, :default => false
|
30
|
+
|
31
|
+
# The default logger for Dynamoid: either the Rails logger or just stdout.
|
32
|
+
#
|
33
|
+
# @since 0.2.0
|
34
|
+
def default_logger
|
35
|
+
defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : ::Logger.new($stdout)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the assigned logger instance.
|
39
|
+
#
|
40
|
+
# @since 0.2.0
|
41
|
+
def logger
|
42
|
+
@logger ||= default_logger
|
43
|
+
end
|
44
|
+
|
45
|
+
# If you want to, set the logger manually to any output you'd like. Or pass false or nil to disable logging entirely.
|
46
|
+
#
|
47
|
+
# @since 0.2.0
|
48
|
+
def logger=(logger)
|
49
|
+
case logger
|
50
|
+
when false, nil then @logger = nil
|
51
|
+
when true then @logger = default_logger
|
52
|
+
else
|
53
|
+
@logger = logger if logger.respond_to?(:info)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -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,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,214 @@
|
|
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 }
|
156
|
+
when 'lt'
|
157
|
+
{ :range_less_than => val }
|
158
|
+
when 'gte'
|
159
|
+
{ :range_gte => val }
|
160
|
+
when 'lte'
|
161
|
+
{ :range_lte => val }
|
162
|
+
when 'between'
|
163
|
+
{ :range_between => val }
|
164
|
+
when 'begins_with'
|
165
|
+
{ :range_begins_with => val }
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def range_query
|
170
|
+
opts = { :hash_value => query[source.hash_key] }
|
171
|
+
query.keys.select { |k| k.to_s.include?('.') }.each do |key|
|
172
|
+
opts.merge!(range_hash(key))
|
173
|
+
end
|
174
|
+
opts.merge(query_opts).merge(consistent_opts)
|
175
|
+
end
|
176
|
+
|
177
|
+
def query_keys
|
178
|
+
query.keys.collect{|k| k.to_s.split('.').first}
|
179
|
+
end
|
180
|
+
|
181
|
+
# [hash_key] or [hash_key, range_key] is specified in query keys.
|
182
|
+
def key_present?
|
183
|
+
query_keys == [source.hash_key.to_s] || (query_keys.to_set == [source.hash_key.to_s, source.range_key.to_s].to_set)
|
184
|
+
end
|
185
|
+
|
186
|
+
def start_key
|
187
|
+
key = { :hash_key_element => @start.hash_key }
|
188
|
+
if range_key = @start.class.range_key
|
189
|
+
key.merge!({:range_key_element => @start.send(range_key) })
|
190
|
+
end
|
191
|
+
key
|
192
|
+
end
|
193
|
+
|
194
|
+
def query_opts
|
195
|
+
opts = {}
|
196
|
+
opts[:select] = 'ALL_ATTRIBUTES'
|
197
|
+
opts[:limit] = @eval_limit if @eval_limit
|
198
|
+
opts[:next_token] = start_key if @start
|
199
|
+
opts[:scan_index_forward] = @scan_index_forward
|
200
|
+
opts
|
201
|
+
end
|
202
|
+
|
203
|
+
def scan_opts
|
204
|
+
opts = {}
|
205
|
+
opts[:limit] = @eval_limit if @eval_limit
|
206
|
+
opts[:next_token] = start_key if @start
|
207
|
+
opts[:batch_size] = @batch_size if @batch_size
|
208
|
+
opts
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
end
|
213
|
+
|
214
|
+
end
|