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,156 @@
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}?") { !read_attribute(named).nil? }
55
+ define_method("#{named}=") {|value| write_attribute(named, value) }
56
+ end
57
+
58
+ def range(name, type = :string)
59
+ field(name, type)
60
+ self.range_key = name
61
+ end
62
+
63
+ def table(options)
64
+ #a default 'id' column is created when Dynamoid::Document is included
65
+ unless(attributes.has_key? hash_key)
66
+ remove_field :id
67
+ field(hash_key)
68
+ end
69
+ end
70
+
71
+ def remove_field(field)
72
+ field = field.to_sym
73
+ attributes.delete(field) or raise "No such field"
74
+ remove_method field
75
+ remove_method :"#{field}="
76
+ remove_method :"#{field}?"
77
+ end
78
+ end
79
+
80
+ # You can access the attributes of an object directly on its attributes method, which is by default an empty hash.
81
+ attr_accessor :attributes
82
+ alias :raw_attributes :attributes
83
+
84
+ # Write an attribute on the object. Also marks the previous value as dirty.
85
+ #
86
+ # @param [Symbol] name the name of the field
87
+ # @param [Object] value the value to assign to that field
88
+ #
89
+ # @since 0.2.0
90
+ def write_attribute(name, value)
91
+ if (size = value.to_s.size) > MAX_ITEM_SIZE
92
+ Dynamoid.logger.warn "DynamoDB can't store items larger than #{MAX_ITEM_SIZE} and the #{name} field has a length of #{size}."
93
+ end
94
+
95
+ if association = @associations[name]
96
+ association.reset
97
+ end
98
+
99
+ attributes[name.to_sym] = value
100
+ end
101
+ alias :[]= :write_attribute
102
+
103
+ # Read an attribute from an object.
104
+ #
105
+ # @param [Symbol] name the name of the field
106
+ #
107
+ # @since 0.2.0
108
+ def read_attribute(name)
109
+ attributes[name.to_sym]
110
+ end
111
+ alias :[] :read_attribute
112
+
113
+ # Updates multiple attibutes at once, saving the object once the updates are complete.
114
+ #
115
+ # @param [Hash] attributes a hash of attributes to update
116
+ #
117
+ # @since 0.2.0
118
+ def update_attributes(attributes)
119
+ attributes.each {|attribute, value| self.write_attribute(attribute, value)} unless attributes.nil? || attributes.empty?
120
+ save
121
+ end
122
+
123
+ # Update a single attribute, saving the object afterwards.
124
+ #
125
+ # @param [Symbol] attribute the attribute to update
126
+ # @param [Object] value the value to assign it
127
+ #
128
+ # @since 0.2.0
129
+ def update_attribute(attribute, value)
130
+ write_attribute(attribute, value)
131
+ save
132
+ end
133
+
134
+ private
135
+
136
+ # Automatically called during the created callback to set the created_at time.
137
+ #
138
+ # @since 0.2.0
139
+ def set_created_at
140
+ self.created_at = DateTime.now
141
+ end
142
+
143
+ # Automatically called during the save callback to set the updated_at time.
144
+ #
145
+ # @since 0.2.0
146
+ def set_updated_at
147
+ self.updated_at = DateTime.now
148
+ end
149
+
150
+ def set_type
151
+ self.type ||= self.class.to_s if self.class.attributes[:type]
152
+ end
153
+
154
+ end
155
+
156
+ end
@@ -0,0 +1,197 @@
1
+ # encoding: utf-8
2
+ module Dynamoid
3
+
4
+ # This module defines the finder methods that hang off the document at the
5
+ # class level, like find, find_by_id, and the method_missing style finders.
6
+ module Finders
7
+ extend ActiveSupport::Concern
8
+
9
+ RANGE_MAP = {
10
+ 'gt' => :range_greater_than,
11
+ 'lt' => :range_less_than,
12
+ 'gte' => :range_gte,
13
+ 'lte' => :range_lte,
14
+ 'begins_with' => :range_begins_with,
15
+ 'between' => :range_between,
16
+ 'eq' => :range_eq
17
+ }
18
+
19
+ module ClassMethods
20
+
21
+ # Find one or many objects, specified by one id or an array of ids.
22
+ #
23
+ # @param [Array/String] *id an array of ids or one single id
24
+ #
25
+ # @return [Dynamoid::Document] one object or an array of objects, depending on whether the input was an array or not
26
+ #
27
+ # @since 0.2.0
28
+ def find(*ids)
29
+ options = if ids.last.is_a? Hash
30
+ ids.slice!(-1)
31
+ else
32
+ {}
33
+ end
34
+ expects_array = ids.first.kind_of?(Array)
35
+
36
+ ids = Array(ids.flatten.uniq)
37
+ if ids.count == 1
38
+ result = self.find_by_id(ids.first, options)
39
+ expects_array ? Array(result) : result
40
+ else
41
+ find_all(ids)
42
+ end
43
+ end
44
+
45
+ # Return objects found by the given array of ids, either hash keys, or hash/range key combinations using BatchGet.
46
+ # Returns empty array if no results found.
47
+ #
48
+ # @param [Array<ID>] ids
49
+ # @param [Hash] options: Passed to the underlying query.
50
+ #
51
+ # @example
52
+ # find all the user with hash key
53
+ # User.find_all(['1', '2', '3'])
54
+ #
55
+ # find all the tweets using hash key and range key with consistent read
56
+ # Tweet.find_all([['1', 'red'], ['1', 'green']], :consistent_read => true)
57
+ def find_all(ids, options = {})
58
+ items = Dynamoid.adapter.read(self.table_name, ids, options)
59
+ items ? items[self.table_name].map{|i| from_database(i)} : []
60
+ end
61
+
62
+ # Find one object directly by id.
63
+ #
64
+ # @param [String] id the id of the object to find
65
+ #
66
+ # @return [Dynamoid::Document] the found object, or nil if nothing was found
67
+ #
68
+ # @since 0.2.0
69
+ def find_by_id(id, options = {})
70
+ if item = Dynamoid.adapter.read(self.table_name, id, options)
71
+ from_database(item)
72
+ else
73
+ nil
74
+ end
75
+ end
76
+
77
+ # Find one object directly by hash and range keys
78
+ #
79
+ # @param [String] hash_key of the object to find
80
+ # @param [String/Number] range_key of the object to find
81
+ #
82
+ def find_by_composite_key(hash_key, range_key, options = {})
83
+ find_by_id(hash_key, options.merge({:range_key => range_key}))
84
+ end
85
+
86
+ # Find all objects by hash and range keys.
87
+ #
88
+ # @example find all ChamberTypes whose level is greater than 1
89
+ # class ChamberType
90
+ # include Dynamoid::Document
91
+ # field :chamber_type, :string
92
+ # range :level, :integer
93
+ # table :key => :chamber_type
94
+ # end
95
+ # ChamberType.find_all_by_composite_key('DustVault', range_greater_than: 1)
96
+ #
97
+ # @param [String] hash_key of the objects to find
98
+ # @param [Hash] options the options for the range key
99
+ # @option options [Range] :range_value find the range key within this range
100
+ # @option options [Number] :range_greater_than find range keys greater than this
101
+ # @option options [Number] :range_less_than find range keys less than this
102
+ # @option options [Number] :range_gte find range keys greater than or equal to this
103
+ # @option options [Number] :range_lte find range keys less than or equal to this
104
+ #
105
+ # @return [Array] an array of all matching items
106
+ #
107
+ def find_all_by_composite_key(hash_key, options = {})
108
+ Dynamoid.adapter.query(self.table_name, options.merge({hash_value: hash_key})).collect do |item|
109
+ from_database(item)
110
+ end
111
+ end
112
+
113
+ # Find all objects by using local secondary or global secondary index
114
+ #
115
+ # @example
116
+ # class User
117
+ # include Dynamoid::Document
118
+ # field :email, :string
119
+ # field :age, :integer
120
+ # field :gender, :string
121
+ # field :rank :number
122
+ # table :key => :email
123
+ # global_secondary_index :hash_key => :age, :range_key => :gender
124
+ # end
125
+ # User.find_all_by_secondary_index(:age => 5, :range => {"rank.lte" => 10})
126
+ #
127
+ # @param [Hash] eg: {:age => 5}
128
+ # @param [Hash] eg: {"rank.lte" => 10}
129
+ # @param [Hash] options - @TODO support more options in future such as
130
+ # query filter, projected keys etc
131
+ # @return [Array] an array of all matching items
132
+ def find_all_by_secondary_index(hash, options = {})
133
+ range = options[:range] || {}
134
+ hash_key_field, hash_key_value = hash.first
135
+ range_key_field, range_key_value = range.first
136
+ range_op_mapped = nil
137
+
138
+ if range_key_field
139
+ range_key_field = range_key_field.to_s
140
+ range_key_op = "eq"
141
+ if range_key_field.include?(".")
142
+ range_key_field, range_key_op = range_key_field.split(".", 2)
143
+ end
144
+ range_op_mapped = RANGE_MAP.fetch(range_key_op)
145
+ end
146
+
147
+ # Find the index
148
+ index = self.find_index(hash_key_field, range_key_field)
149
+ raise Dynamoid::Errors::MissingIndex if index.nil?
150
+
151
+ # query
152
+ opts = {
153
+ :hash_key => hash_key_field.to_s,
154
+ :hash_value => hash_key_value,
155
+ :index_name => index.name,
156
+ }
157
+ if range_key_field
158
+ opts[:range_key] = range_key_field
159
+ opts[range_op_mapped] = range_key_value
160
+ end
161
+ Dynamoid.adapter.query(self.table_name, opts).map do |item|
162
+ from_database(item)
163
+ end
164
+ end
165
+
166
+ # Find using exciting method_missing finders attributes. Uses criteria chains under the hood to accomplish this neatness.
167
+ #
168
+ # @example find a user by a first name
169
+ # User.find_by_first_name('Josh')
170
+ #
171
+ # @example find all users by first and last name
172
+ # User.find_all_by_first_name_and_last_name('Josh', 'Symonds')
173
+ #
174
+ # @return [Dynamoid::Document/Array] the found object, or an array of found objects if all was somewhere in the method
175
+ #
176
+ # @since 0.2.0
177
+ def method_missing(method, *args)
178
+ if method =~ /find/
179
+ finder = method.to_s.split('_by_').first
180
+ attributes = method.to_s.split('_by_').last.split('_and_')
181
+
182
+ chain = Dynamoid::Criteria::Chain.new(self)
183
+ chain.query = Hash.new.tap {|h| attributes.each_with_index {|attr, index| h[attr.to_sym] = args[index]}}
184
+
185
+ if finder =~ /all/
186
+ return chain.all
187
+ else
188
+ return chain.first
189
+ end
190
+ else
191
+ super
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+ end
@@ -0,0 +1,92 @@
1
+ module Dynamoid
2
+ module IdentityMap
3
+ extend ActiveSupport::Concern
4
+
5
+ def self.clear
6
+ Dynamoid.included_models.each { |m| m.identity_map.clear }
7
+ end
8
+
9
+ module ClassMethods
10
+ def identity_map
11
+ @identity_map ||= {}
12
+ end
13
+
14
+ def from_database(attrs = {})
15
+ return super if identity_map_off?
16
+
17
+ key = identity_map_key(attrs)
18
+ document = identity_map[key]
19
+
20
+ if document.nil?
21
+ document = super
22
+ identity_map[key] = document
23
+ else
24
+ document.load(attrs)
25
+ end
26
+
27
+ document
28
+ end
29
+
30
+ def find_by_id(id, options = {})
31
+ return super if identity_map_off?
32
+
33
+ key = id.to_s
34
+
35
+ if range_key = options[:range_key]
36
+ key += "::#{range_key}"
37
+ end
38
+
39
+ if identity_map[key]
40
+ identity_map[key]
41
+ else
42
+ super
43
+ end
44
+ end
45
+
46
+ def identity_map_key(attrs)
47
+ key = attrs[hash_key].to_s
48
+ if range_key
49
+ key += "::#{attrs[range_key]}"
50
+ end
51
+ key
52
+ end
53
+
54
+ def identity_map_on?
55
+ Dynamoid::Config.identity_map
56
+ end
57
+
58
+ def identity_map_off?
59
+ !identity_map_on?
60
+ end
61
+ end
62
+
63
+ def identity_map
64
+ self.class.identity_map
65
+ end
66
+
67
+ def save(*args)
68
+ return super if self.class.identity_map_off?
69
+
70
+ if result = super
71
+ identity_map[identity_map_key] = self
72
+ end
73
+ result
74
+ end
75
+
76
+ def delete
77
+ return super if self.class.identity_map_off?
78
+
79
+ identity_map.delete(identity_map_key)
80
+ super
81
+ end
82
+
83
+
84
+ def identity_map_key
85
+ key = hash_key.to_s
86
+ if self.class.range_key
87
+ key += "::#{range_value}"
88
+ end
89
+ key
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,16 @@
1
+ module Dynamoid
2
+ module Middleware
3
+ class IdentityMap
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ Dynamoid::IdentityMap.clear
10
+ @app.call(env)
11
+ ensure
12
+ Dynamoid::IdentityMap.clear
13
+ end
14
+ end
15
+ end
16
+ end