dyna_model 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +81 -0
- data/Rakefile +11 -0
- data/dyna_model.gemspec +31 -0
- data/lib/dyna_model/attributes.rb +187 -0
- data/lib/dyna_model/aws/record/attributes/serialized_attr.rb +33 -0
- data/lib/dyna_model/config/options.rb +78 -0
- data/lib/dyna_model/config.rb +46 -0
- data/lib/dyna_model/document.rb +169 -0
- data/lib/dyna_model/extensions/symbol.rb +11 -0
- data/lib/dyna_model/persistence.rb +77 -0
- data/lib/dyna_model/query.rb +207 -0
- data/lib/dyna_model/response.rb +36 -0
- data/lib/dyna_model/schema.rb +251 -0
- data/lib/dyna_model/table.rb +453 -0
- data/lib/dyna_model/tasks.rb +81 -0
- data/lib/dyna_model/validations.rb +33 -0
- data/lib/dyna_model/version.rb +3 -0
- data/lib/dyna_model.rb +33 -0
- data/spec/app/models/cacher.rb +14 -0
- data/spec/app/models/callbacker.rb +46 -0
- data/spec/app/models/user.rb +28 -0
- data/spec/app/models/validez.rb +33 -0
- data/spec/dyna_model/attributes_spec.rb +54 -0
- data/spec/dyna_model/callbacks_spec.rb +35 -0
- data/spec/dyna_model/persistence_spec.rb +81 -0
- data/spec/dyna_model/query_spec.rb +118 -0
- data/spec/dyna_model/validations_spec.rb +61 -0
- data/spec/spec_helper.rb +58 -0
- metadata +197 -0
@@ -0,0 +1,453 @@
|
|
1
|
+
module DynaModel
|
2
|
+
class Table
|
3
|
+
|
4
|
+
extend AWS::DynamoDB::Types
|
5
|
+
|
6
|
+
attr_reader :table_schema, :client, :schema_loaded_from_dynamo, :hash_key, :range_keys
|
7
|
+
|
8
|
+
RETURNED_CONSUMED_CAPACITY = {
|
9
|
+
none: "NONE",
|
10
|
+
total: "TOTAL"
|
11
|
+
}
|
12
|
+
|
13
|
+
TYPE_INDICATOR = {
|
14
|
+
b: "B",
|
15
|
+
n: "N",
|
16
|
+
s: "S",
|
17
|
+
ss: "SS",
|
18
|
+
ns: "NS"
|
19
|
+
}
|
20
|
+
|
21
|
+
QUERY_SELECT = {
|
22
|
+
all: "ALL_ATTRIBUTES",
|
23
|
+
projected: "ALL_PROJECTED_ATTRIBUTES",
|
24
|
+
count: "COUNT",
|
25
|
+
specific: "SPECIFIC_ATTRIBUTES"
|
26
|
+
}
|
27
|
+
|
28
|
+
COMPARISON_OPERATOR = {
|
29
|
+
eq: "EQ",
|
30
|
+
le: "LE",
|
31
|
+
lt: "LT",
|
32
|
+
ge: "GE",
|
33
|
+
gt: "GT",
|
34
|
+
begins_with: "BEGINS_WITH",
|
35
|
+
between: "BETWEEN",
|
36
|
+
# Scan only
|
37
|
+
ne: "NE",
|
38
|
+
not_null: "NOT_NULL",
|
39
|
+
null: "NULL",
|
40
|
+
contains: "CONTAINS",
|
41
|
+
not_contains: "NOT_CONTAINS",
|
42
|
+
in: "IN"
|
43
|
+
}
|
44
|
+
|
45
|
+
COMPARISON_OPERATOR_SCAN_ONLY = [
|
46
|
+
:ne,
|
47
|
+
:not_null,
|
48
|
+
:null,
|
49
|
+
:contains,
|
50
|
+
:not_contains,
|
51
|
+
:in
|
52
|
+
]
|
53
|
+
|
54
|
+
class << self
|
55
|
+
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.type_from_value(value)
|
60
|
+
case
|
61
|
+
when value.kind_of?(AWS::DynamoDB::Binary) then :b
|
62
|
+
when value.respond_to?(:to_str) then :s
|
63
|
+
when value.kind_of?(Numeric) then :n
|
64
|
+
else
|
65
|
+
raise ArgumentError, "unsupported attribute type #{value.class}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.attr_with_type(attr_name, value)
|
70
|
+
{ attr_name => { TYPE_INDICATOR[type_from_value(value)] => value.to_s } }
|
71
|
+
end
|
72
|
+
|
73
|
+
def initialize(model)
|
74
|
+
@model = model
|
75
|
+
@table_schema = model.table_schema
|
76
|
+
self.load_schema
|
77
|
+
self.validate_key_schema
|
78
|
+
end
|
79
|
+
|
80
|
+
def load_schema
|
81
|
+
@schema_loaded_from_dynamo = @model.describe_table
|
82
|
+
|
83
|
+
@schema_loaded_from_dynamo[:table][:key_schema].each do |key|
|
84
|
+
key_attr = @table_schema[:attribute_definitions].find{|h| h[:attribute_name] == key[:attribute_name]}
|
85
|
+
next if key_attr.nil?
|
86
|
+
key_schema_attr = {
|
87
|
+
attribute_name: key[:attribute_name],
|
88
|
+
attribute_type: key_attr[:attribute_type]
|
89
|
+
}
|
90
|
+
|
91
|
+
if key[:key_type] == "HASH"
|
92
|
+
@hash_key = key_schema_attr
|
93
|
+
else
|
94
|
+
(@range_keys ||= []) << key_schema_attr.merge(:primary_range_key => true)
|
95
|
+
@primary_range_key = key_schema_attr.merge(:primary_range_key => true)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
if @schema_loaded_from_dynamo[:table][:local_secondary_indexes] || @schema_loaded_from_dynamo[:table][:global_secondary_indexes]
|
100
|
+
((@schema_loaded_from_dynamo[:table][:local_secondary_indexes] || []) + (@schema_loaded_from_dynamo[:table][:global_secondary_indexes] || [])).each do |key|
|
101
|
+
si_range_key = key[:key_schema].find{|h| h[:key_type] == "RANGE" }
|
102
|
+
next if si_range_key.nil?
|
103
|
+
si_range_attribute = @table_schema[:attribute_definitions].find{|h| h[:attribute_name] == si_range_key[:attribute_name]}
|
104
|
+
next if si_range_attribute.nil?
|
105
|
+
(@range_keys ||= []) << {
|
106
|
+
attribute_name: si_range_key[:attribute_name],
|
107
|
+
attribute_type: si_range_attribute[:attribute_type],
|
108
|
+
index_name: key[:index_name]
|
109
|
+
}
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
@schema_loaded_from_dynamo
|
114
|
+
end
|
115
|
+
|
116
|
+
def validate_key_schema
|
117
|
+
if @schema_loaded_from_dynamo[:table][:key_schema].sort_by { |k| k[:key_type] } != @table_schema[:key_schema].sort_by { |k| k[:key_type] }
|
118
|
+
raise ArgumentError, "It appears your key schema (Hash Key/Range Key) have changed from the table definition. Rebuilding the table is necessary."
|
119
|
+
end
|
120
|
+
|
121
|
+
if @schema_loaded_from_dynamo[:table][:attribute_definitions].sort_by { |k| k[:attribute_name] } != @table_schema[:attribute_definitions].sort_by { |k| k[:attribute_name] }
|
122
|
+
raise ArgumentError, "It appears your attribute definition (types?) have changed from the table definition. Rebuilding the table is necessary."
|
123
|
+
end
|
124
|
+
|
125
|
+
index_keys_to_reject = [:index_status, :index_size_bytes, :item_count]
|
126
|
+
|
127
|
+
if @schema_loaded_from_dynamo[:table][:local_secondary_indexes].blank? != @table_schema[:local_secondary_indexes].blank?
|
128
|
+
raise ArgumentError, "It appears your local secondary indexes have changed from the table definition. Rebuilding the table is necessary."
|
129
|
+
end
|
130
|
+
|
131
|
+
if @schema_loaded_from_dynamo[:table][:local_secondary_indexes] && (@schema_loaded_from_dynamo[:table][:local_secondary_indexes].dup.collect {|i| i.delete_if{|k, v| index_keys_to_reject.include?(k) }; i }.sort_by { |lsi| lsi[:index_name] } != @table_schema[:local_secondary_indexes].sort_by { |lsi| lsi[:index_name] })
|
132
|
+
raise ArgumentError, "It appears your local secondary indexes have changed from the table definition. Rebuilding the table is necessary."
|
133
|
+
end
|
134
|
+
|
135
|
+
if @schema_loaded_from_dynamo[:table][:global_secondary_indexes].blank? != @table_schema[:global_secondary_indexes].blank?
|
136
|
+
raise ArgumentError, "It appears your global secondary indexes have changed from the table definition. Rebuilding the table is necessary."
|
137
|
+
end
|
138
|
+
|
139
|
+
if @schema_loaded_from_dynamo[:table][:global_secondary_indexes] && (@schema_loaded_from_dynamo[:table][:global_secondary_indexes].dup.collect {|i| i.delete_if{|k, v| index_keys_to_reject.include?(k) }; i }.sort_by { |gsi| gsi[:index_name] } != @table_schema[:global_secondary_indexes].sort_by { |gsi| gsi[:index_name] })
|
140
|
+
raise ArgumentError, "It appears your global secondary indexes have changed from the table definition. Rebuilding the table is necessary."
|
141
|
+
end
|
142
|
+
|
143
|
+
if @schema_loaded_from_dynamo[:table][:provisioned_throughput][:read_capacity_units] != @table_schema[:provisioned_throughput][:read_capacity_units]
|
144
|
+
Toy::Dynamo::Config.logger.error "read_capacity_units mismatch. Need to update table?"
|
145
|
+
end
|
146
|
+
|
147
|
+
if @schema_loaded_from_dynamo[:table][:provisioned_throughput][:write_capacity_units] != @table_schema[:provisioned_throughput][:write_capacity_units]
|
148
|
+
Toy::Dynamo::Config.logger.error "write_capacity_units mismatch. Need to update table?"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def hash_key_item_param(value)
|
153
|
+
hash_key = @table_schema[:key_schema].find{|h| h[:key_type] == "HASH"}[:attribute_name]
|
154
|
+
hash_key_type = @table_schema[:attribute_definitions].find{|h| h[:attribute_name] == hash_key}[:attribute_type]
|
155
|
+
{ hash_key => { hash_key_type => value.to_s } }
|
156
|
+
end
|
157
|
+
|
158
|
+
def hash_key_condition_param(hash_key, value)
|
159
|
+
hash_key_type = @table_schema[:attribute_definitions].find{|h| h[:attribute_name] == hash_key}[:attribute_type]
|
160
|
+
{
|
161
|
+
hash_key => {
|
162
|
+
attribute_value_list: [hash_key_type => value.to_s],
|
163
|
+
comparison_operator: COMPARISON_OPERATOR[:eq]
|
164
|
+
}
|
165
|
+
}
|
166
|
+
end
|
167
|
+
|
168
|
+
def get_item(hash_key, options={})
|
169
|
+
options[:consistent_read] = false unless options[:consistent_read]
|
170
|
+
options[:return_consumed_capacity] ||= :none # "NONE" # || "TOTAL"
|
171
|
+
options[:select] ||= []
|
172
|
+
|
173
|
+
get_item_request = {
|
174
|
+
table_name: @model.dynamo_db_table_name(options[:shard_name]),
|
175
|
+
key: hash_key_item_param(hash_key),
|
176
|
+
consistent_read: options[:consistent_read],
|
177
|
+
return_consumed_capacity: RETURNED_CONSUMED_CAPACITY[options[:return_consumed_capacity]]
|
178
|
+
}
|
179
|
+
get_item_request.merge!( attributes_to_get: [options[:select]].flatten ) unless options[:select].blank?
|
180
|
+
@model.dynamo_db_client.get_item(get_item_request)
|
181
|
+
end
|
182
|
+
|
183
|
+
# == options
|
184
|
+
# * consistent_read
|
185
|
+
# * return_consumed_capacity
|
186
|
+
# * order
|
187
|
+
# * select
|
188
|
+
# * range
|
189
|
+
def query(hash_value, options={})
|
190
|
+
options[:consistent_read] = false unless options[:consistent_read]
|
191
|
+
options[:return_consumed_capacity] ||= :none # "NONE" # || "TOTAL"
|
192
|
+
options[:order] ||= :desc
|
193
|
+
#options[:index_name] ||= :none
|
194
|
+
#AWS::DynamoDB::Errors::ValidationException: ALL_PROJECTED_ATTRIBUTES can be used only when Querying using an IndexName
|
195
|
+
#options[:limit] ||= 10
|
196
|
+
#options[:exclusive_start_key]
|
197
|
+
|
198
|
+
key_conditions = {}
|
199
|
+
gsi = nil
|
200
|
+
if options[:global_secondary_index]
|
201
|
+
gsi = @table_schema[:global_secondary_indexes].select{ |gsi| gsi[:index_name].to_s == options[:global_secondary_index].to_s}.first
|
202
|
+
raise ArgumentError, "Could not find Global Secondary Index '#{options[:global_secondary_index]}'" unless gsi
|
203
|
+
gsi_hash_key = gsi[:key_schema].find{|h| h[:key_type] == "HASH"}[:attribute_name]
|
204
|
+
key_conditions.merge!(hash_key_condition_param(gsi_hash_key, hash_value))
|
205
|
+
else
|
206
|
+
hash_key = @table_schema[:key_schema].find{|h| h[:key_type] == "HASH"}[:attribute_name]
|
207
|
+
key_conditions.merge!(hash_key_condition_param(hash_key, hash_value))
|
208
|
+
end
|
209
|
+
|
210
|
+
query_request = {
|
211
|
+
table_name: @model.dynamo_db_table_name(options[:shard_name]),
|
212
|
+
key_conditions: key_conditions,
|
213
|
+
consistent_read: options[:consistent_read],
|
214
|
+
return_consumed_capacity: RETURNED_CONSUMED_CAPACITY[options[:return_consumed_capacity]],
|
215
|
+
scan_index_forward: (options[:order] == :asc)
|
216
|
+
}
|
217
|
+
|
218
|
+
if options[:range]
|
219
|
+
raise ArgumentError, "Expected a 2 element Hash for :range (ex {:age.gt => 13})" unless options[:range].is_a?(Hash) && options[:range].keys.size == 1 && options[:range].keys.first.is_a?(String)
|
220
|
+
range_key_name, comparison_operator = options[:range].keys.first.split(".")
|
221
|
+
raise ArgumentError, "Comparison operator must be one of (#{(COMPARISON_OPERATOR.keys - COMPARISON_OPERATOR_SCAN_ONLY).join(", ")})" unless COMPARISON_OPERATOR.keys.include?(comparison_operator.to_sym)
|
222
|
+
range_key = @range_keys.find{|k| k[:attribute_name] == range_key_name}
|
223
|
+
raise ArgumentError, ":range key must be a valid Range attribute" unless range_key
|
224
|
+
raise ArgumentError, ":range key must be a Range if using the operator BETWEEN" if comparison_operator == "between" && !options[:range].values.first.is_a?(Range)
|
225
|
+
|
226
|
+
if range_key.has_key?(:index_name) # Local/Global Secondary Index
|
227
|
+
options[:index_name] = range_key[:index_name]
|
228
|
+
query_request[:index_name] = range_key[:index_name]
|
229
|
+
end
|
230
|
+
|
231
|
+
range_value = options[:range].values.first
|
232
|
+
range_attribute_list = []
|
233
|
+
if comparison_operator == "between"
|
234
|
+
range_attribute_list << { range_key[:attribute_type] => range_value.min }
|
235
|
+
range_attribute_list << { range_key[:attribute_type] => range_value.max }
|
236
|
+
else
|
237
|
+
# TODO - support Binary?
|
238
|
+
range_attribute_list = [{ range_key[:attribute_type] => range_value.to_s }]
|
239
|
+
end
|
240
|
+
|
241
|
+
key_conditions.merge!({
|
242
|
+
range_key[:attribute_name] => {
|
243
|
+
attribute_value_list: range_attribute_list,
|
244
|
+
comparison_operator: COMPARISON_OPERATOR[comparison_operator.to_sym]
|
245
|
+
}
|
246
|
+
})
|
247
|
+
end
|
248
|
+
|
249
|
+
if options[:global_secondary_index] # Override index_name if using GSI
|
250
|
+
# You can only select projected attributes from a GSI
|
251
|
+
options[:select] = :projected #if options[:select].blank?
|
252
|
+
options[:index_name] = gsi[:index_name]
|
253
|
+
query_request.merge!(index_name: gsi[:index_name])
|
254
|
+
end
|
255
|
+
options[:select] ||= :all # :all, :projected, :count, []
|
256
|
+
if options[:select].is_a?(Array)
|
257
|
+
attrs_to_select = [options[:select].map(&:to_s)].flatten
|
258
|
+
attrs_to_select << @hash_key[:attribute_name]
|
259
|
+
attrs_to_select << @primary_range_key[:attribute_name] if @primary_range_key
|
260
|
+
query_request.merge!({
|
261
|
+
select: QUERY_SELECT[:specific],
|
262
|
+
attributes_to_get: attrs_to_select.uniq
|
263
|
+
})
|
264
|
+
else
|
265
|
+
query_request.merge!({ select: QUERY_SELECT[options[:select]] })
|
266
|
+
end
|
267
|
+
|
268
|
+
query_request.merge!({ limit: options[:limit].to_i }) if options.has_key?(:limit)
|
269
|
+
query_request.merge!({ exclusive_start_key: options[:exclusive_start_key] }) if options[:exclusive_start_key]
|
270
|
+
|
271
|
+
@model.dynamo_db_client.query(query_request)
|
272
|
+
end
|
273
|
+
|
274
|
+
def batch_get_item(keys, options={})
|
275
|
+
options[:return_consumed_capacity] ||= :none
|
276
|
+
options[:select] ||= []
|
277
|
+
options[:consistent_read] = false unless options[:consistent_read]
|
278
|
+
|
279
|
+
raise ArgumentError, "must include between 1 - 100 keys" if keys.size == 0 || keys.size > 100
|
280
|
+
keys_request = []
|
281
|
+
keys.each do |k|
|
282
|
+
key_request = {}
|
283
|
+
if @primary_range_key
|
284
|
+
hash_value = k[:hash_value]
|
285
|
+
else
|
286
|
+
raise ArgumentError, "expected keys to be in the form of ['hash key here'] for table with no range keys" if hash_value.is_a?(Hash)
|
287
|
+
hash_value = k
|
288
|
+
end
|
289
|
+
raise ArgumentError, "every key must include a :hash_value" if hash_value.blank?
|
290
|
+
key_request[@hash_key[:attribute_name]] = { @hash_key[:attribute_type] => hash_value.to_s }
|
291
|
+
if @primary_range_key
|
292
|
+
range_value = k[:range_value]
|
293
|
+
raise ArgumentError, "every key must include a :range_value" if range_value.blank?
|
294
|
+
key_request[@primary_range_key[:attribute_name]] = { @primary_range_key[:attribute_type] => range_value.to_s }
|
295
|
+
end
|
296
|
+
keys_request << key_request
|
297
|
+
end
|
298
|
+
|
299
|
+
request_items_request = {}
|
300
|
+
request_items_request.merge!( keys: keys_request )
|
301
|
+
request_items_request.merge!( attributes_to_get: [options[:select]].flatten ) unless options[:select].blank?
|
302
|
+
request_items_request.merge!( consistent_read: options[:consistent_read] ) if options[:consistent_read]
|
303
|
+
batch_get_item_request = {
|
304
|
+
request_items: { @model.dynamo_db_table_name(options[:shard_name]) => request_items_request },
|
305
|
+
return_consumed_capacity: RETURNED_CONSUMED_CAPACITY[options[:return_consumed_capacity]]
|
306
|
+
}
|
307
|
+
@model.dynamo_db_client.batch_get_item(batch_get_item_request)
|
308
|
+
end
|
309
|
+
|
310
|
+
def write(attributes, options={})
|
311
|
+
options[:return_consumed_capacity] ||= :none
|
312
|
+
options[:update_item] = false unless options[:update_item]
|
313
|
+
|
314
|
+
if options[:update_item]
|
315
|
+
# UpdateItem
|
316
|
+
key_request = {
|
317
|
+
@hash_key[:attribute_name] => {
|
318
|
+
@hash_key[:attribute_type] => options[:update_item][:hash_value].to_s,
|
319
|
+
}
|
320
|
+
}
|
321
|
+
if @primary_range_key
|
322
|
+
raise ArgumentError, "range_key was not provided to the write command" if options[:update_item][:range_value].blank?
|
323
|
+
key_request.merge!({
|
324
|
+
@primary_range_key[:attribute_name] => {
|
325
|
+
@primary_range_key[:attribute_type] => options[:update_item][:range_value].to_s
|
326
|
+
}
|
327
|
+
})
|
328
|
+
end
|
329
|
+
attrs_to_update = {}
|
330
|
+
attributes.each_pair do |k,v|
|
331
|
+
next if k == @hash_key[:attribute_name] || (@primary_range_key && k == @primary_range_key[:attribute_name])
|
332
|
+
if v.nil?
|
333
|
+
attrs_to_update.merge!({ k => { :action => "DELETE" } })
|
334
|
+
else
|
335
|
+
attrs_to_update.merge!({
|
336
|
+
k => {
|
337
|
+
value: self.class.attr_with_type(k,v).values.last,
|
338
|
+
action: "PUT"
|
339
|
+
}
|
340
|
+
})
|
341
|
+
end
|
342
|
+
end
|
343
|
+
update_item_request = {
|
344
|
+
table_name: @model.dynamo_db_table_name(options[:shard_name]),
|
345
|
+
key: key_request,
|
346
|
+
attribute_updates: attrs_to_update,
|
347
|
+
return_consumed_capacity: RETURNED_CONSUMED_CAPACITY[options[:return_consumed_capacity]]
|
348
|
+
}
|
349
|
+
@model.dynamo_db_client.update_item(update_item_request)
|
350
|
+
else
|
351
|
+
# PutItem
|
352
|
+
items = {}
|
353
|
+
attributes.each_pair do |k,v|
|
354
|
+
next if v.blank? # If empty string or nil, skip...
|
355
|
+
items.merge!(self.class.attr_with_type(k,v))
|
356
|
+
end
|
357
|
+
put_item_request = {
|
358
|
+
table_name: @model.dynamo_db_table_name(options[:shard_name]),
|
359
|
+
item: items,
|
360
|
+
return_consumed_capacity: RETURNED_CONSUMED_CAPACITY[options[:return_consumed_capacity]]
|
361
|
+
}
|
362
|
+
@model.dynamo_db_client.put_item(put_item_request)
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
def delete_item(options={})
|
367
|
+
raise ":delete_item => {...key_values...} required" unless options[:delete_item].present?
|
368
|
+
key_request = {
|
369
|
+
@hash_key[:attribute_name] => {
|
370
|
+
@hash_key[:attribute_type] => options[:delete_item][:hash_value].to_s
|
371
|
+
}
|
372
|
+
}
|
373
|
+
if @primary_range_key
|
374
|
+
raise ArgumentError, "range_key was not provided to the delete_item command" if options[:delete_item][:range_value].blank?
|
375
|
+
key_request.merge!({
|
376
|
+
@primary_range_key[:attribute_name] => {
|
377
|
+
@primary_range_key[:attribute_type] => options[:delete_item][:range_value].to_s
|
378
|
+
}
|
379
|
+
})
|
380
|
+
end
|
381
|
+
delete_item_request = {
|
382
|
+
table_name: @model.dynamo_db_table_name(options[:shard_name]),
|
383
|
+
key: key_request
|
384
|
+
}
|
385
|
+
@model.dynamo_db_client.delete_item(delete_item_request)
|
386
|
+
end
|
387
|
+
|
388
|
+
# Perform a table scan
|
389
|
+
# http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Scan.html
|
390
|
+
def scan(options={})
|
391
|
+
options[:return_consumed_capacity] ||= :none # "NONE" # || "TOTAL"
|
392
|
+
# Default if not already set
|
393
|
+
options[:select] ||= :all # :all, :projected, :count, []
|
394
|
+
|
395
|
+
scan_request = {
|
396
|
+
table_name: @model.dynamo_db_table_name(options[:shard_name]),
|
397
|
+
return_consumed_capacity: RETURNED_CONSUMED_CAPACITY[options[:return_consumed_capacity]]
|
398
|
+
}
|
399
|
+
|
400
|
+
scan_request.merge!({ limit: options[:limit].to_i }) if options.has_key?(:limit)
|
401
|
+
scan_request.merge!({ exclusive_start_key: options[:exclusive_start_key] }) if options[:exclusive_start_key]
|
402
|
+
|
403
|
+
if options[:select].is_a?(Array)
|
404
|
+
attrs_to_select = [options[:select].map(&:to_s)].flatten
|
405
|
+
attrs_to_select << @hash_key[:attribute_name]
|
406
|
+
attrs_to_select << @primary_range_key[:attribute_name] if @primary_range_key
|
407
|
+
scan_request.merge!({
|
408
|
+
select: QUERY_SELECT[:specific],
|
409
|
+
attributes_to_get: attrs_to_select.uniq
|
410
|
+
})
|
411
|
+
else
|
412
|
+
scan_request.merge!({ select: QUERY_SELECT[options[:select]] })
|
413
|
+
end
|
414
|
+
|
415
|
+
# :scan_filter => { :name.begins_with => "a" }
|
416
|
+
scan_filter = {}
|
417
|
+
if options[:scan_filter].present?
|
418
|
+
options[:scan_filter].each_pair.each do |k,v|
|
419
|
+
# Hard to validate attribute types here, so infer by type sent and assume the user knows their own attrs
|
420
|
+
key_name, comparison_operator = k.split(".")
|
421
|
+
raise ArgumentError, "Comparison operator must be one of (#{COMPARISON_OPERATOR.keys.join(", ")})" unless COMPARISON_OPERATOR.keys.include?(comparison_operator.to_sym)
|
422
|
+
raise ArgumentError, "scan_filter value must be a Range if using the operator BETWEEN" if comparison_operator == "between" && !v.is_a?(Range)
|
423
|
+
raise ArgumentError, "scan_filter value must be a Array if using the operator IN" if comparison_operator == "in" && !v.is_a?(Array)
|
424
|
+
|
425
|
+
attribute_value_list = []
|
426
|
+
if comparison_operator == "in"
|
427
|
+
v.each do |in_v|
|
428
|
+
attribute_value_list << self.class.attr_with_type(key_name, in_v).values.last
|
429
|
+
end
|
430
|
+
elsif comparison_operator == "between"
|
431
|
+
attribute_value_list << self.class.attr_with_type(key_name, v.min).values.last
|
432
|
+
attribute_value_list << self.class.attr_with_type(key_name, v.max).values.last
|
433
|
+
else
|
434
|
+
attribute_value_list << self.class.attr_with_type(key_name, v).values.last
|
435
|
+
end
|
436
|
+
scan_filter.merge!({
|
437
|
+
key_name => {
|
438
|
+
comparison_operator: COMPARISON_OPERATOR[comparison_operator.to_sym],
|
439
|
+
attribute_value_list: attribute_value_list
|
440
|
+
}
|
441
|
+
})
|
442
|
+
end
|
443
|
+
scan_request.merge!(scan_filter: scan_filter)
|
444
|
+
end
|
445
|
+
|
446
|
+
scan_request.merge!({ segment: options[:segment].to_i }) if options[:segment].present?
|
447
|
+
scan_request.merge!({ total_segments: options[:total_segments].to_i }) if options[:total_segments].present?
|
448
|
+
|
449
|
+
@model.dynamo_db_client.scan(scan_request)
|
450
|
+
end
|
451
|
+
|
452
|
+
end
|
453
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
module DynaModel
|
4
|
+
module Tasks
|
5
|
+
extend self
|
6
|
+
def included_models
|
7
|
+
dir = ENV['DIR'].to_s != '' ? ENV['DIR'] : Rails.root.join("app/models")
|
8
|
+
puts "Loading models from: #{dir}"
|
9
|
+
included = []
|
10
|
+
Dir.glob(File.join("#{dir}/**/*.rb")).each do |path|
|
11
|
+
model_filename = path[/#{Regexp.escape(dir.to_s)}\/([^\.]+).rb/, 1]
|
12
|
+
next if model_filename.match(/^concerns\//i) # Skip concerns/ folder
|
13
|
+
|
14
|
+
begin
|
15
|
+
klass = model_filename.camelize.constantize
|
16
|
+
rescue NameError
|
17
|
+
require(path) ? retry : raise
|
18
|
+
rescue LoadError => e
|
19
|
+
# Try non-namespaced class name instead...
|
20
|
+
klass = model_filename.camelize.split("::").last.constantize
|
21
|
+
end
|
22
|
+
|
23
|
+
# Skip if the class doesn't have DynaModel integration
|
24
|
+
next unless klass.respond_to?(:dynamo_db_table)
|
25
|
+
|
26
|
+
included << klass
|
27
|
+
end
|
28
|
+
included
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
namespace :ddb do
|
34
|
+
desc 'Create a DynamoDB table'
|
35
|
+
task :create => :environment do
|
36
|
+
raise "expected usage: rake ddb:create CLASS=User" unless ENV['CLASS']
|
37
|
+
options = {}
|
38
|
+
options.merge!(shard_name: ENV['SHARD']) if ENV['SHARD']
|
39
|
+
if ENV["CLASS"] == "all"
|
40
|
+
DynaModel::Tasks.included_models.each do |klass|
|
41
|
+
puts "Creating table for #{klass}..."
|
42
|
+
begin
|
43
|
+
klass.create_table(options)
|
44
|
+
rescue Exception => e
|
45
|
+
puts "Could not create table! #{e.inspect}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
else
|
49
|
+
ENV['CLASS'].constantize.create_table(options)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
desc 'Resize a DynamoDB table read/write provision'
|
54
|
+
task :resize => :environment do
|
55
|
+
raise "expected usage: rake ddb:resize CLASS=User" unless ENV['CLASS']
|
56
|
+
options = {}
|
57
|
+
options.merge!(shard_name: ENV['SHARD']) if ENV['SHARD']
|
58
|
+
options.merge!(read_capacity_units: ENV['READ'].to_i) if ENV['READ']
|
59
|
+
options.merge!(write_capacity_units: ENV['WRITE'].to_i) if ENV['WRITE']
|
60
|
+
ENV['CLASS'].constantize.resize_table(options)
|
61
|
+
end
|
62
|
+
|
63
|
+
desc 'Destroy a DynamoDB table'
|
64
|
+
task :destroy => :environment do
|
65
|
+
raise "expected usage: rake ddb:destroy CLASS=User" unless ENV['CLASS']
|
66
|
+
options = {}
|
67
|
+
options.merge!(shard_name: ENV['SHARD']) if ENV['SHARD']
|
68
|
+
if ENV["CLASS"] == "all"
|
69
|
+
DynaModel::Tasks.included_models.each do |klass|
|
70
|
+
puts "Destroying table for #{klass}..."
|
71
|
+
begin
|
72
|
+
klass.delete_table(options)
|
73
|
+
rescue Exception => e
|
74
|
+
puts "Could not create table! #{e.inspect}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
else
|
78
|
+
ENV['CLASS'].constantize.delete_table(options)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module AWS
|
2
|
+
module Record
|
3
|
+
module AbstractBase
|
4
|
+
|
5
|
+
# OVERRIDE
|
6
|
+
# https://github.com/aws/aws-sdk-ruby/blob/master/lib/aws/record/abstract_base.rb#L20
|
7
|
+
# Disable aws-sdk validations in favor of ActiveModel::Validations
|
8
|
+
def self.extended base
|
9
|
+
base.send(:extend, ClassMethods)
|
10
|
+
base.send(:include, InstanceMethods)
|
11
|
+
base.send(:include, DirtyTracking)
|
12
|
+
#base.send(:extend, Validations)
|
13
|
+
|
14
|
+
# these 3 modules are for rails 3+ active model compatability
|
15
|
+
base.send(:extend, Naming)
|
16
|
+
base.send(:include, Naming)
|
17
|
+
base.send(:include, Conversion)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module DynaModel
|
25
|
+
module Validations
|
26
|
+
extend ActiveSupport::Concern
|
27
|
+
include ActiveModel::Validations
|
28
|
+
include ActiveModel::Validations::Callbacks
|
29
|
+
|
30
|
+
module ClassMethods
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/dyna_model.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require "aws-sdk"
|
2
|
+
require "rails"
|
3
|
+
require "active_support"
|
4
|
+
require 'active_support/concern'
|
5
|
+
require 'active_model'
|
6
|
+
require "dyna_model/aws/record/attributes/serialized_attr"
|
7
|
+
require "dyna_model/version"
|
8
|
+
require "dyna_model/tasks"
|
9
|
+
require "dyna_model/config"
|
10
|
+
require "dyna_model/attributes"
|
11
|
+
require "dyna_model/schema"
|
12
|
+
require "dyna_model/persistence"
|
13
|
+
require "dyna_model/table"
|
14
|
+
require "dyna_model/query"
|
15
|
+
require "dyna_model/response"
|
16
|
+
require "dyna_model/validations"
|
17
|
+
require "dyna_model/extensions/symbol"
|
18
|
+
require "dyna_model/document"
|
19
|
+
|
20
|
+
module DynaModel
|
21
|
+
|
22
|
+
extend self
|
23
|
+
|
24
|
+
def configure
|
25
|
+
block_given? ? yield(DynaModel::Config) : DynaModel::Config
|
26
|
+
end
|
27
|
+
alias :config :configure
|
28
|
+
|
29
|
+
def logger
|
30
|
+
DynaModel::Config.logger
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class Callbacker
|
2
|
+
|
3
|
+
include DynaModel::Document
|
4
|
+
|
5
|
+
string_attr :id
|
6
|
+
boolean_attr :before_create_block_attr, default_value: false
|
7
|
+
boolean_attr :before_create_method_attr, default_value: false
|
8
|
+
boolean_attr :before_validation_on_create_method_attr, default_value: false
|
9
|
+
validates_inclusion_of :before_validation_on_create_method_attr, in: [true]
|
10
|
+
integer_attr :before_save_counter, default_value: 0
|
11
|
+
integer_attr :before_update_counter, default_value: 0
|
12
|
+
timestamps
|
13
|
+
|
14
|
+
hash_key :id
|
15
|
+
|
16
|
+
read_provision 2
|
17
|
+
write_provision 8
|
18
|
+
|
19
|
+
before_create :before_create_method
|
20
|
+
before_create do
|
21
|
+
self.before_create_block_attr = true
|
22
|
+
end
|
23
|
+
before_validation :before_validation_on_create_method, on: :create
|
24
|
+
after_create :after_create_change_before_validation
|
25
|
+
|
26
|
+
before_save do
|
27
|
+
self.before_save_counter += 1
|
28
|
+
end
|
29
|
+
|
30
|
+
before_update do
|
31
|
+
self.before_update_counter += 1
|
32
|
+
end
|
33
|
+
|
34
|
+
def before_create_method
|
35
|
+
self.before_create_method_attr = true
|
36
|
+
end
|
37
|
+
|
38
|
+
def before_validation_on_create_method
|
39
|
+
self.before_validation_on_create_method_attr = true
|
40
|
+
end
|
41
|
+
|
42
|
+
def after_create_change_before_validation
|
43
|
+
self.before_validation_on_create_method_attr = false
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|