dyna_model 0.0.1

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