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