lotus-dynamodb 0.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,211 @@
1
+ require 'lotus/utils/kernel'
2
+ require 'multi_json'
3
+
4
+ module Lotus
5
+ module Model
6
+ module Adapters
7
+ module Dynamodb
8
+ # Translates values from/to the database with the corresponding Ruby type.
9
+ #
10
+ # @api private
11
+ # @since 0.1.0
12
+ class Coercer < Lotus::Model::Mapping::Coercer
13
+ SKIPPED_KLASSES = [Float, Integer, Set, String]
14
+ SUPPORTED_KLASSES = [AWS::DynamoDB::Binary, Array, Boolean, Date, DateTime, Hash, Time]
15
+
16
+ # Converts value from given type to DynamoDB record value.
17
+ #
18
+ # @api private
19
+ # @since 0.1.0
20
+ def from_aws_dynamodb_binary(value)
21
+ return value if value.nil? || value.is_a?(AWS::DynamoDB::Binary)
22
+ AWS::DynamoDB::Binary.new(value)
23
+ end
24
+
25
+ alias_method :to_aws_dynamodb_binary, :from_aws_dynamodb_binary
26
+
27
+ # Converts value from given type to DynamoDB record value.
28
+ #
29
+ # @api private
30
+ # @since 0.1.0
31
+ def from_array(value)
32
+ _serialize(value)
33
+ end
34
+
35
+ # Converts value from DynamoDB record value to given type.
36
+ #
37
+ # @api private
38
+ # @since 0.1.0
39
+ def to_array(value)
40
+ _deserialize(value)
41
+ end
42
+
43
+ # Converts value from given type to DynamoDB record value.
44
+ #
45
+ # @api private
46
+ # @since 0.1.0
47
+ def from_boolean(value)
48
+ value ? 1 : 0
49
+ end
50
+
51
+ # Converts value from DynamoDB record value to given type.
52
+ #
53
+ # @api private
54
+ # @since 0.1.0
55
+ def to_boolean(value)
56
+ value.to_i == 1
57
+ end
58
+
59
+ # Converts value from given type to DynamoDB record value.
60
+ #
61
+ # @api private
62
+ # @since 0.1.0
63
+ def from_date(value)
64
+ value.to_time.to_i
65
+ end
66
+
67
+ # Converts value from DynamoDB record value to given type.
68
+ #
69
+ # @api private
70
+ # @since 0.1.0
71
+ def to_date(value)
72
+ Time.at(value.to_i).to_date
73
+ end
74
+
75
+ # Converts value from given type to DynamoDB record value.
76
+ #
77
+ # @api private
78
+ # @since 0.1.0
79
+ def from_datetime(value)
80
+ value.to_time.to_f
81
+ end
82
+
83
+ # Converts value from DynamoDB record value to given type.
84
+ #
85
+ # @api private
86
+ # @since 0.1.0
87
+ def to_datetime(value)
88
+ Time.at(value.to_f).to_datetime
89
+ end
90
+
91
+ # Converts value from given type to DynamoDB record value.
92
+ #
93
+ # @api private
94
+ # @since 0.1.0
95
+ def from_hash(value)
96
+ _serialize(value)
97
+ end
98
+
99
+ # Converts value from DynamoDB record value to given type.
100
+ #
101
+ # @api private
102
+ # @since 0.1.0
103
+ def to_hash(value)
104
+ _deserialize(value)
105
+ end
106
+
107
+ # Converts value from given type to DynamoDB record value.
108
+ #
109
+ # @api private
110
+ # @since 0.1.0
111
+ def from_time(value)
112
+ value.to_f
113
+ end
114
+
115
+ # Converts value from DynamoDB record value to given type.
116
+ #
117
+ # @api private
118
+ # @since 0.1.0
119
+ def to_time(value)
120
+ Time.at(value.to_f)
121
+ end
122
+
123
+ private
124
+ # Compile itself for performance boost.
125
+ #
126
+ # @api private
127
+ # @since 0.1.0
128
+ def _compile!
129
+ instance_eval(SKIPPED_KLASSES.map do |klass|
130
+ %{
131
+ def from_#{_method_name(klass)}(value)
132
+ value
133
+ end
134
+
135
+ def to_#{_method_name(klass)}(value)
136
+ value
137
+ end
138
+ }
139
+ end.join("\n"))
140
+
141
+ code = @collection.attributes.map do |_,(klass,mapped)|
142
+ %{
143
+ def deserialize_#{ mapped }(value)
144
+ #{kernel_wrap(klass) { "from_#{_method_name(klass)}(value)" }}
145
+ end
146
+
147
+ def serialize_#{ mapped }(value)
148
+ from_#{_method_name(klass)}(value)
149
+ end
150
+ }
151
+ end.join("\n")
152
+
153
+ instance_eval %{
154
+ def to_record(entity)
155
+ if entity.id
156
+ Hash[*[#{ @collection.attributes.map{|name,(klass,mapped)| ":#{mapped},from_#{_method_name(klass)}(entity.#{name})"}.join(',') }]]
157
+ else
158
+ Hash[*[#{ @collection.attributes.reject{|name,_| name == @collection.identity }.map{|name,(klass,mapped)| ":#{mapped},from_#{_method_name(klass)}(entity.#{name})"}.join(',') }]]
159
+ end
160
+ end
161
+
162
+ def from_record(record)
163
+ #{ @collection.entity }.new(
164
+ Hash[*[#{ @collection.attributes.map{|name,(klass,mapped)| ":#{name},#{kernel_wrap(klass) { "to_#{_method_name(klass)}(record[:#{mapped}])" }}"}.join(',') }]]
165
+ )
166
+ end
167
+
168
+ #{ code }
169
+ }
170
+ end
171
+
172
+ # Wraps string in Lotus::Utils::Kernel call if needed.
173
+ #
174
+ # @api private
175
+ # @since 0.1.0
176
+ def kernel_wrap(klass)
177
+ if klass.to_s.include?("::")
178
+ yield
179
+ else
180
+ "Lotus::Utils::Kernel.#{klass}(#{yield})"
181
+ end
182
+ end
183
+
184
+ # Returns method name from klass.
185
+ #
186
+ # @api private
187
+ # @since 0.1.0
188
+ def _method_name(klass)
189
+ klass.to_s.downcase.gsub("::", "_")
190
+ end
191
+
192
+ # Serializes value to string.
193
+ #
194
+ # @api private
195
+ # @since 0.1.0
196
+ def _serialize(value)
197
+ MultiJson.dump(value)
198
+ end
199
+
200
+ # Deserializes value from string.
201
+ #
202
+ # @api private
203
+ # @since 0.1.0
204
+ def _deserialize(value)
205
+ MultiJson.load(value)
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,321 @@
1
+ require 'securerandom'
2
+ require 'aws-sdk'
3
+ require 'lotus/utils/hash'
4
+
5
+ module Lotus
6
+ module Model
7
+ module Adapters
8
+ module Dynamodb
9
+ # Acts like table, using AWS::DynamoDB::Client.
10
+ #
11
+ # @api private
12
+ # @since 0.1.0
13
+ class Collection
14
+ include AWS::DynamoDB::Types
15
+
16
+ # Response interface provides count and entities.
17
+ #
18
+ # @api private
19
+ # @since 0.1.0
20
+ class Response
21
+ attr_accessor :count, :entities
22
+
23
+ def initialize(count, entities = nil)
24
+ @count, @entities = count, entities
25
+ end
26
+ end
27
+
28
+ # @attr_reader name [String] the name of the collection (eg. `users`)
29
+ #
30
+ # @since 0.1.0
31
+ # @api private
32
+ attr_reader :name
33
+
34
+ # @attr_reader identity [Symbol] the primary key of the collection
35
+ # (eg. `:id`)
36
+ #
37
+ # @since 0.1.0
38
+ # @api private
39
+ attr_reader :identity
40
+
41
+ # Initialize a collection.
42
+ #
43
+ # @param client [AWS::DynamoDB::Client] DynamoDB client
44
+ # @param coercer [Lotus::Model::Adapters::Dynamodb::Coercer]
45
+ # @param name [Symbol] the name of the collection (eg. `:users`)
46
+ # @param identity [Symbol] the primary key of the collection
47
+ # (eg. `:id`).
48
+ #
49
+ # @api private
50
+ # @since 0.1.0
51
+ def initialize(client, coercer, name, identity)
52
+ @client, @coercer = client, coercer
53
+ @name, @identity = name.to_s, identity
54
+ @key_schema = {}
55
+ end
56
+
57
+ # Creates a record for the given entity and returns a primary key.
58
+ #
59
+ # @param entity [Object] the entity to persist
60
+ #
61
+ # @see Lotus::Model::Adapters::Dynamodb::Command#create
62
+ # @see http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/DynamoDB/Client/V20120810.html#put_item-instance_method
63
+ #
64
+ # @return the primary key of the just created record.
65
+ #
66
+ # @api private
67
+ # @since 0.1.0
68
+ def create(entity)
69
+ entity[identity] ||= SecureRandom.uuid
70
+
71
+ @client.put_item(
72
+ table_name: name,
73
+ item: serialize_item(entity),
74
+ )
75
+
76
+ entity[identity]
77
+ end
78
+
79
+ # Updates the record corresponding to the given entity.
80
+ #
81
+ # @param entity [Object] the entity to persist
82
+ #
83
+ # @see Lotus::Model::Adapters::Dynamodb::Command#update
84
+ # @see http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/DynamoDB/Client/V20120810.html#update_item-instance_method
85
+ #
86
+ # @api private
87
+ # @since 0.1.0
88
+ def update(entity)
89
+ @client.update_item(
90
+ table_name: name,
91
+ key: serialize_key(entity),
92
+ attribute_updates: serialize_attributes(entity),
93
+ )
94
+ end
95
+
96
+ # Deletes the record corresponding to the given entity.
97
+ #
98
+ # @param entity [Object] the entity to delete
99
+ #
100
+ # @see Lotus::Model::Adapters::Dynamodb::Command#delete
101
+ # @see http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/DynamoDB/Client/V20120810.html#delete_item-instance_method
102
+ #
103
+ # @api private
104
+ # @since 0.1.0
105
+ def delete(entity)
106
+ @client.delete_item(
107
+ table_name: name,
108
+ key: serialize_key(entity),
109
+ )
110
+ end
111
+
112
+ # Returns an unique record from the given collection, with the given
113
+ # id.
114
+ #
115
+ # @param key [Array] the identity of the object
116
+ #
117
+ # @see Lotus::Model::Adapters::Dynamodb::Command#get
118
+ # @see http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/DynamoDB/Client/V20120810.html#get_item-instance_method
119
+ #
120
+ # @return [Hash] the serialized record
121
+ #
122
+ # @api private
123
+ # @since 0.1.0
124
+ def get(key)
125
+ return if key.any? { |v| v.to_s == "" }
126
+ return if key.count != key_schema.count
127
+
128
+ response = @client.get_item(
129
+ table_name: name,
130
+ key: serialize_key(key),
131
+ )
132
+
133
+ deserialize_item(response[:item]) if response[:item]
134
+ end
135
+
136
+ # Performs DynamoDB query operation.
137
+ #
138
+ # @param options [Hash] AWS::DynamoDB::Client options
139
+ #
140
+ # @see http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/DynamoDB/Client/V20120810.html#query-instance_method
141
+ #
142
+ # @return [Array<Hash>] the serialized entities
143
+ #
144
+ # @api private
145
+ # @since 0.1.0
146
+ def query(options = {})
147
+ response = @client.query(options.merge(table_name: name))
148
+ deserialize_response(response)
149
+ end
150
+
151
+ # Performs DynamoDB scan operation.
152
+ #
153
+ # @param options [Hash] AWS::DynamoDB::Client options
154
+ #
155
+ # @see http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/DynamoDB/Client/V20120810.html#scan-instance_method
156
+ #
157
+ # @return [Array<Hash>] the serialized entities
158
+ #
159
+ # @api private
160
+ # @since 0.1.0
161
+ def scan(options = {})
162
+ response = @client.scan(options.merge(table_name: name))
163
+ deserialize_response(response)
164
+ end
165
+
166
+ # Fetches DynamoDB table schema.
167
+ #
168
+ # @see http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/DynamoDB/Client/V20120810.html#describe_table-instance_method
169
+ #
170
+ # @return [Hash] table schema definition
171
+ #
172
+ # @api private
173
+ # @since 0.1.0
174
+ def schema
175
+ @schema ||= @client.describe_table(table_name: name).fetch(:table)
176
+ end
177
+
178
+ # Maps table key schema to hash with attribute name as key and key
179
+ # type as value.
180
+ #
181
+ # @param index [String] index to check (defaults to table itself)
182
+ #
183
+ # @see Lotus::Model::Adapters::Dynamodb::Collection#schema
184
+ #
185
+ # @return [Hash] key schema definition
186
+ #
187
+ # @api private
188
+ # @since 0.1.0
189
+ def key_schema(index = nil)
190
+ return @key_schema[index] if @key_schema[index]
191
+
192
+ current_schema = if index
193
+ everything = Array(schema[:local_secondary_indexes]) +
194
+ Array(schema[:global_secondary_indexes])
195
+ indexes = Hash[everything.map { |i| [i[:index_name], i] }]
196
+ indexes[index][:key_schema]
197
+ else
198
+ schema[:key_schema]
199
+ end
200
+
201
+ @key_schema[index] ||= Hash[current_schema.to_a.map do |key|
202
+ [key[:attribute_name].to_sym, key[:key_type]]
203
+ end]
204
+ end
205
+
206
+ # Checks if given column is in key schema or not.
207
+ #
208
+ # @param column [String] column to check
209
+ # @param index [String] index to check (defaults to table itself)
210
+ #
211
+ # @see Lotus::Model::Adapters::Dynamodb::Collection#key_schema
212
+ #
213
+ # @return [Boolean]
214
+ #
215
+ # @api private
216
+ # @since 0.1.0
217
+ def key?(column, index = nil)
218
+ key_schema(index).has_key?(column)
219
+ end
220
+
221
+ # Coerce and format attribute value to match DynamoDB type.
222
+ #
223
+ # @param column [String] the attribute column
224
+ # @param value [Object] the attribute value
225
+ #
226
+ # @see AWS::DynamoDB::Types
227
+ #
228
+ # @return [Hash] the formatted attribute
229
+ #
230
+ # @api private
231
+ # @since 0.1.0
232
+ def format_attribute(column, value)
233
+ value = @coercer.public_send(:"serialize_#{ column }", value)
234
+ format_attribute_value(value)
235
+ end
236
+
237
+ # Serialize given record to have proper attributes for 'item' query.
238
+ #
239
+ # @param record [Hash] the serialized record
240
+ #
241
+ # @see AWS::DynamoDB::Types
242
+ #
243
+ # @return [Hash] the serialized item
244
+ #
245
+ # @api private
246
+ # @since 0.1.0
247
+ def serialize_item(record)
248
+ Hash[record.map { |k, v| [k.to_s, format_attribute_value(v)] }]
249
+ end
250
+
251
+ # Serialize given record or primary key to have proper attributes
252
+ # for 'key' query.
253
+ #
254
+ # @param record [Hash,Array] the serialized record or primary key
255
+ #
256
+ # @see AWS::DynamoDB::Types
257
+ #
258
+ # @return [Hash] the serialized key
259
+ #
260
+ # @api private
261
+ # @since 0.1.0
262
+ def serialize_key(record)
263
+ Hash[key_schema.keys.each_with_index.map do |k, idx|
264
+ v = record.is_a?(Hash) ? record[k] : record[idx]
265
+ [k.to_s, format_attribute(k, v)]
266
+ end]
267
+ end
268
+
269
+ # Serialize given entity to exclude key schema attributes.
270
+ #
271
+ # @param entity [Hash] the entity
272
+ #
273
+ # @see AWS::DynamoDB::Types
274
+ #
275
+ # @return [Hash] the serialized attributes
276
+ #
277
+ # @api private
278
+ # @since 0.1.0
279
+ def serialize_attributes(entity)
280
+ keys = key_schema.keys
281
+ Hash[entity.reject { |k, _| keys.include?(k) }.map do |k, v|
282
+ [k.to_s, { value: format_attribute_value(v), action: "PUT" }]
283
+ end]
284
+ end
285
+
286
+ # Deserialize DynamoDB scan/query response.
287
+ #
288
+ # @param response [Hash] the serialized response
289
+ #
290
+ # @return [Hash] the deserialized response
291
+ #
292
+ # @api private
293
+ # @since 0.1.0
294
+ def deserialize_response(response)
295
+ result = Response.new(response[:count])
296
+
297
+ result.entities = response[:member].map do |item|
298
+ deserialize_item(item)
299
+ end if response[:member]
300
+
301
+ result
302
+ end
303
+
304
+ # Deserialize item from DynamoDB response.
305
+ #
306
+ # @param item [Hash] the serialized item
307
+ #
308
+ # @see AWS::DynamoDB::Types
309
+ #
310
+ # @return [Hash] the deserialized record
311
+ #
312
+ # @api private
313
+ # @since 0.1.0
314
+ def deserialize_item(record)
315
+ Lotus::Utils::Hash.new(values_from_response_hash(record)).symbolize!
316
+ end
317
+ end
318
+ end
319
+ end
320
+ end
321
+ end
@@ -0,0 +1,117 @@
1
+ module Lotus
2
+ module Model
3
+ module Adapters
4
+ module Dynamodb
5
+ # Execute a command for the given collection.
6
+ #
7
+ # @see Lotus::Model::Adapters::Dynamodb::Collection
8
+ # @see Lotus::Model::Mapping::Collection
9
+ #
10
+ # @api private
11
+ # @since 0.1.0
12
+ class Command
13
+ # Initialize a command.
14
+ #
15
+ # @param dataset [Lotus::Model::Adapters::Dynamodb::Collection]
16
+ # @param collection [Lotus::Model::Mapping::Collection]
17
+ #
18
+ # @return [Lotus::Model::Adapters::Dynamodb::Command]
19
+ #
20
+ # @api private
21
+ # @since 0.1.0
22
+ def initialize(dataset, collection)
23
+ @dataset, @collection = dataset, collection
24
+ end
25
+
26
+ # Creates a record for the given entity.
27
+ #
28
+ # @param entity [Object] the entity to persist
29
+ #
30
+ # @see Lotus::Model::Adapters::Dynamodb::Collection#create
31
+ #
32
+ # @return the primary key of the just created record.
33
+ #
34
+ # @api private
35
+ # @since 0.1.0
36
+ def create(entity)
37
+ @dataset.create(
38
+ _serialize(entity)
39
+ )
40
+ end
41
+
42
+ # Updates the corresponding record for the given entity.
43
+ #
44
+ # @param entity [Object] the entity to persist
45
+ #
46
+ # @see Lotus::Model::Adapters::Dynamodb::Collection#update
47
+ #
48
+ # @api private
49
+ # @since 0.1.0
50
+ def update(entity)
51
+ @dataset.update(
52
+ _serialize(entity)
53
+ )
54
+ end
55
+
56
+ # Deletes the corresponding record for the given entity.
57
+ #
58
+ # @param entity [Object] the entity to delete
59
+ #
60
+ # @see Lotus::Model::Adapters::Dynamodb::Collection#delete
61
+ #
62
+ # @api private
63
+ # @since 0.1.0
64
+ def delete(entity)
65
+ @dataset.delete(
66
+ _serialize(entity)
67
+ )
68
+ end
69
+
70
+ # Returns an unique record from the given collection, with the given
71
+ # id.
72
+ #
73
+ # @param key [Array] the identity of the object
74
+ #
75
+ # @see Lotus::Model::Adapters::Dynamodb::Collection#get
76
+ #
77
+ # @return [Object] the entity
78
+ #
79
+ # @api private
80
+ # @since 0.1.0
81
+ def get(key)
82
+ @collection.deserialize(
83
+ [@dataset.get(key)].compact
84
+ ).first
85
+ end
86
+
87
+ # Deletes all the records from the table.
88
+ #
89
+ # @see Lotus::Model::Adapters::Dynamodb::Collection#scan
90
+ # @see Lotus::Model::Mapping::Collection
91
+ #
92
+ # @api private
93
+ # @since 0.1.0
94
+ def clear
95
+ @collection.deserialize(@dataset.scan.entities).each do |entity|
96
+ delete(entity)
97
+ end
98
+ end
99
+
100
+ private
101
+ # Serialize the given entity before to persist in the database.
102
+ #
103
+ # @param entity [Object] the entity
104
+ #
105
+ # @return [Hash] the serialized entity
106
+ #
107
+ # @api private
108
+ # @since 0.1.0
109
+ def _serialize(entity)
110
+ serialized = @collection.serialize(entity)
111
+ serialized.delete_if { |_, v| v.nil? }
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end