dyna_model 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ module DynaModel
2
+ VERSION = "0.0.1"
3
+ 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,14 @@
1
+ class Cacher
2
+
3
+ include DynaModel::Document
4
+
5
+ string_attr :key
6
+ string_attr :body
7
+ timestamps
8
+
9
+ hash_key :key
10
+
11
+ read_provision 4
12
+ write_provision 4
13
+
14
+ 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