dynamoid-edge 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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