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,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