synamoid 1.2.1 → 1.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,199 +0,0 @@
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 => :rank
124
- # end
125
- # # NOTE: the first param and the second param are both hashes,
126
- # # so curly braces must be used on first hash param if sending both params
127
- # User.find_all_by_secondary_index({:age => 5}, :range => {"rank.lte" => 10})
128
- #
129
- # @param [Hash] eg: {:age => 5}
130
- # @param [Hash] eg: {"rank.lte" => 10}
131
- # @param [Hash] options - query filter, projected keys, scan_index_forward etc
132
- # @return [Array] an array of all matching items
133
- def find_all_by_secondary_index(hash, options = {})
134
- range = options[:range] || {}
135
- hash_key_field, hash_key_value = hash.first
136
- range_key_field, range_key_value = range.first
137
- range_op_mapped = nil
138
-
139
- if range_key_field
140
- range_key_field = range_key_field.to_s
141
- range_key_op = "eq"
142
- if range_key_field.include?(".")
143
- range_key_field, range_key_op = range_key_field.split(".", 2)
144
- end
145
- range_op_mapped = RANGE_MAP.fetch(range_key_op)
146
- end
147
-
148
- # Find the index
149
- index = self.find_index(hash_key_field, range_key_field)
150
- raise Dynamoid::Errors::MissingIndex.new("attempted to find #{[hash_key_field, range_key_field]}") if index.nil?
151
-
152
- # query
153
- opts = {
154
- :hash_key => hash_key_field.to_s,
155
- :hash_value => hash_key_value,
156
- :index_name => index.name,
157
- }
158
- if range_key_field
159
- opts[:range_key] = range_key_field
160
- opts[range_op_mapped] = range_key_value
161
- end
162
- dynamo_options = opts.merge(options.reject {|key, _| key == :range })
163
- Dynamoid.adapter.query(self.table_name, dynamo_options).map do |item|
164
- from_database(item)
165
- end
166
- end
167
-
168
- # Find using exciting method_missing finders attributes. Uses criteria chains under the hood to accomplish this neatness.
169
- #
170
- # @example find a user by a first name
171
- # User.find_by_first_name('Josh')
172
- #
173
- # @example find all users by first and last name
174
- # User.find_all_by_first_name_and_last_name('Josh', 'Symonds')
175
- #
176
- # @return [Dynamoid::Document/Array] the found object, or an array of found objects if all was somewhere in the method
177
- #
178
- # @since 0.2.0
179
- def method_missing(method, *args)
180
- if method =~ /find/
181
- finder = method.to_s.split('_by_').first
182
- attributes = method.to_s.split('_by_').last.split('_and_')
183
-
184
- chain = Dynamoid::Criteria::Chain.new(self)
185
- chain.query = Hash.new.tap {|h| attributes.each_with_index {|attr, index| h[attr.to_sym] = args[index]}}
186
-
187
- if finder =~ /all/
188
- return chain.all
189
- else
190
- return chain.first
191
- end
192
- else
193
- super
194
- end
195
- end
196
- end
197
- end
198
-
199
- end
@@ -1,92 +0,0 @@
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
@@ -1,273 +0,0 @@
1
- module Dynamoid
2
- module Indexes
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- class_attribute :local_secondary_indexes
7
- class_attribute :global_secondary_indexes
8
- self.local_secondary_indexes = {}
9
- self.global_secondary_indexes = {}
10
- end
11
-
12
- module ClassMethods
13
- # Defines a Global Secondary index on a table. Keys can be specified as
14
- # hash-only, or hash & range.
15
- #
16
- # @param [Hash] options options to pass for this table
17
- # @option options [Symbol] :name the name for the index; this still gets
18
- # namespaced. If not specified, will use a default name.
19
- # @option options [Symbol] :hash_key the index hash key column.
20
- # @option options [Symbol] :range_key the index range key column (if
21
- # applicable).
22
- # @option options [Symbol, Array<Symbol>] :projected_attributes table
23
- # attributes to project for this index. Can be :keys_only, :all
24
- # or an array of included fields. If not specified, defaults to
25
- # :keys_only.
26
- # @option options [Integer] :read_capacity set the read capacity for the
27
- # index; does not work on existing indexes.
28
- # @option options [Integer] :write_capacity set the write capacity for
29
- # the index; does not work on existing indexes.
30
- def global_secondary_index(options={})
31
- unless options.present?
32
- raise Dynamoid::Errors::InvalidIndex.new('empty index definition')
33
- end
34
-
35
- unless options[:hash_key].present?
36
- raise Dynamoid::Errors::InvalidIndex.new(
37
- 'A global secondary index requires a :hash_key to be specified'
38
- )
39
- end
40
-
41
- index_opts = {
42
- :read_capacity => Dynamoid::Config.read_capacity,
43
- :write_capacity => Dynamoid::Config.write_capacity
44
- }.merge(options)
45
-
46
- index_opts[:dynamoid_class] = self
47
- index_opts[:type] = :global_secondary
48
-
49
- index = Dynamoid::Indexes::Index.new(index_opts)
50
- gsi_key = index_key(options[:hash_key], options[:range_key])
51
- self.global_secondary_indexes[gsi_key] = index
52
- self
53
- end
54
-
55
-
56
- # Defines a local secondary index on a table. Will use the same primary
57
- # hash key as the table.
58
- #
59
- # @param [Hash] options options to pass for this index.
60
- # @option options [Symbol] :name the name for the index; this still gets
61
- # namespaced. If not specified, a name is automatically generated.
62
- # @option options [Symbol] :range_key the range key column for the index.
63
- # @option options [Symbol, Array<Symbol>] :projected_attributes table
64
- # attributes to project for this index. Can be :keys_only, :all
65
- # or an array of included fields. If not specified, defaults to
66
- # :keys_only.
67
- def local_secondary_index(options={})
68
- unless options.present?
69
- raise Dynamoid::Errors::InvalidIndex.new('empty index definition')
70
- end
71
-
72
- primary_hash_key = self.hash_key
73
- primary_range_key = self.range_key
74
- index_range_key = options[:range_key]
75
-
76
- unless index_range_key.present?
77
- raise Dynamoid::Errors::InvalidIndex.new('A local secondary index '\
78
- 'requires a :range_key to be specified')
79
- end
80
-
81
- if primary_range_key.present? && index_range_key == primary_range_key
82
- raise Dynamoid::Errors::InvalidIndex.new('A local secondary index'\
83
- ' must use a different :range_key than the primary key')
84
- end
85
-
86
- index_opts = options.merge({
87
- :dynamoid_class => self,
88
- :type => :local_secondary,
89
- :hash_key => primary_hash_key
90
- })
91
-
92
- index = Dynamoid::Indexes::Index.new(index_opts)
93
- key = index_key(primary_hash_key, index_range_key)
94
- self.local_secondary_indexes[key] = index
95
- self
96
- end
97
-
98
-
99
- def find_index(hash, range=nil)
100
- index = self.indexes[index_key(hash, range)]
101
- index
102
- end
103
-
104
-
105
- # Returns true iff the provided hash[,range] key combo is a local
106
- # secondary index.
107
- #
108
- # @param [Symbol] hash hash key name.
109
- # @param [Symbol] range range key name.
110
- # @return [Boolean] true iff provided keys correspond to a local
111
- # secondary index.
112
- def is_local_secondary_index?(hash, range=nil)
113
- self.local_secondary_indexes[index_key(hash, range)].present?
114
- end
115
-
116
-
117
- # Returns true iff the provided hash[,range] key combo is a global
118
- # secondary index.
119
- #
120
- # @param [Symbol] hash hash key name.
121
- # @param [Symbol] range range key name.
122
- # @return [Boolean] true iff provided keys correspond to a global
123
- # secondary index.
124
- def is_global_secondary_index?(hash, range=nil)
125
- self.global_secondary_indexes[index_key(hash, range)].present?
126
- end
127
-
128
-
129
- # Generates a convenient lookup key name for a hash/range index.
130
- # Should normally not be used directly.
131
- #
132
- # @param [Symbol] hash hash key name.
133
- # @param [Symbol] range range key name.
134
- # @return [String] returns "hash" if hash only, "hash_range" otherwise.
135
- def index_key(hash, range=nil)
136
- name = hash.to_s
137
- if range.present?
138
- name += "_#{range.to_s}"
139
- end
140
- name
141
- end
142
-
143
-
144
- # Generates a default index name.
145
- #
146
- # @param [Symbol] hash hash key name.
147
- # @param [Symbol] range range key name.
148
- # @return [String] index name of the form "table_name_index_index_key".
149
- def index_name(hash, range=nil)
150
- "#{self.table_name}_index_#{self.index_key(hash, range)}"
151
- end
152
-
153
-
154
- # Convenience method to return all indexes on the table.
155
- #
156
- # @return [Hash<String, Object>] the combined hash of global and local
157
- # secondary indexes.
158
- def indexes
159
- self.local_secondary_indexes.merge(self.global_secondary_indexes)
160
- end
161
-
162
- def indexed_hash_keys
163
- self.global_secondary_indexes.map do |name, index|
164
- index.hash_key.to_s
165
- end
166
- end
167
- end
168
-
169
-
170
- # Represents the attributes of a DynamoDB index.
171
- class Index
172
- include ActiveModel::Validations
173
-
174
- PROJECTION_TYPES = [:keys_only, :all].to_set
175
- DEFAULT_PROJECTION_TYPE = :keys_only
176
-
177
- attr_accessor :name, :dynamoid_class, :type, :hash_key, :range_key,
178
- :hash_key_schema, :range_key_schema, :projected_attributes,
179
- :read_capacity, :write_capacity
180
-
181
-
182
- validate do
183
- validate_index_type
184
- validate_hash_key
185
- validate_range_key
186
- validate_projected_attributes
187
- end
188
-
189
-
190
- def initialize(attrs={})
191
- unless attrs[:dynamoid_class].present?
192
- raise Dynamoid::Errors::InvalidIndex.new(':dynamoid_class is required')
193
- end
194
-
195
- @dynamoid_class = attrs[:dynamoid_class]
196
- @type = attrs[:type]
197
- @hash_key = attrs[:hash_key]
198
- @range_key = attrs[:range_key]
199
- @name = attrs[:name] || @dynamoid_class.index_name(@hash_key, @range_key)
200
- @projected_attributes =
201
- attrs[:projected_attributes] || DEFAULT_PROJECTION_TYPE
202
- @read_capacity = attrs[:read_capacity]
203
- @write_capacity = attrs[:write_capacity]
204
-
205
- raise Dynamoid::Errors::InvalidIndex.new(self) unless self.valid?
206
- end
207
-
208
-
209
- # Convenience method to determine the projection type for an index.
210
- # Projection types are: :keys_only, :all, :include.
211
- #
212
- # @return [Symbol] the projection type.
213
- def projection_type
214
- if @projected_attributes.is_a? Array
215
- :include
216
- else
217
- @projected_attributes
218
- end
219
- end
220
-
221
-
222
- private
223
-
224
- def validate_projected_attributes
225
- unless (@projected_attributes.is_a?(Array) ||
226
- PROJECTION_TYPES.include?(@projected_attributes))
227
- errors.add(:projected_attributes, 'Invalid projected attributes specified.')
228
- end
229
- end
230
-
231
- def validate_index_type
232
- unless (@type.present? &&
233
- [:local_secondary, :global_secondary].include?(@type))
234
- errors.add(:type, 'Invalid index :type specified')
235
- end
236
- end
237
-
238
- def validate_range_key
239
- if @range_key.present?
240
- range_field_attributes = @dynamoid_class.attributes[@range_key]
241
- if range_field_attributes.present?
242
- range_key_type = range_field_attributes[:type]
243
- if Dynamoid::Fields::PERMITTED_KEY_TYPES.include?(range_key_type)
244
- @range_key_schema = {
245
- @range_key => @dynamoid_class.dynamo_type(range_key_type)
246
- }
247
- else
248
- errors.add(:range_key, 'Index :range_key is not a valid key type')
249
- end
250
- else
251
- errors.add(:range_key, "No such field #{@range_key} defined on table")
252
- end
253
- end
254
- end
255
-
256
- def validate_hash_key
257
- hash_field_attributes = @dynamoid_class.attributes[@hash_key]
258
- if hash_field_attributes.present?
259
- hash_field_type = hash_field_attributes[:type]
260
- if Dynamoid::Fields::PERMITTED_KEY_TYPES.include?(hash_field_type)
261
- @hash_key_schema = {
262
- @hash_key => @dynamoid_class.dynamo_type(hash_field_type)
263
- }
264
- else
265
- errors.add(:hash_key, 'Index :hash_key is not a valid key type')
266
- end
267
- else
268
- errors.add(:hash_key, "No such field #{@hash_key} defined on table")
269
- end
270
- end
271
- end
272
- end
273
- end