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.
@@ -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