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,77 @@
|
|
1
|
+
# TODO: optimistic locking?
|
2
|
+
|
3
|
+
module DynaModel
|
4
|
+
module Persistence
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
private
|
8
|
+
def populate_id
|
9
|
+
#@_id = UUIDTools::UUID.random_create.to_s.downcase
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def dynamo_db_table
|
14
|
+
self.class.dynamo_db_table(shard)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def create_storage
|
19
|
+
run_callbacks :save do
|
20
|
+
run_callbacks :create do
|
21
|
+
self.class.dynamo_db_table.write(serialize_attributes)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def update_storage
|
28
|
+
# Only enumerating dirty (i.e. changed) attributes. Empty
|
29
|
+
# (nil and empty set) values are deleted, the others are replaced.
|
30
|
+
attr_updates = {}
|
31
|
+
changed.each do |attr_name|
|
32
|
+
attribute = self.class.attribute_for(attr_name)
|
33
|
+
value = serialize_attribute(attribute, @_data[attr_name])
|
34
|
+
if value.nil? or value == []
|
35
|
+
attr_updates[attr_name] = nil
|
36
|
+
else
|
37
|
+
attr_updates[attr_name] = value
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
run_callbacks :save do
|
42
|
+
run_callbacks :update do
|
43
|
+
self.class.dynamo_db_table.write(attr_updates, {
|
44
|
+
update_item: dynamo_db_item_key_values,
|
45
|
+
shard_name: self.shard
|
46
|
+
})
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
def delete_storage
|
53
|
+
run_callbacks :destroy do
|
54
|
+
self.class.dynamo_db_table.delete_item(
|
55
|
+
delete_item: dynamo_db_item_key_values,
|
56
|
+
shard_name: self.shard
|
57
|
+
)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
def deserialize_item_data data
|
63
|
+
data.inject({}) do |hash,(attr_name,value)|
|
64
|
+
if attribute = self.class.attributes[attr_name]
|
65
|
+
hash[attr_name] = value.is_a?(Set) ?
|
66
|
+
value.map{|v| attribute.deserialize(v) } :
|
67
|
+
attribute.deserialize(value)
|
68
|
+
end
|
69
|
+
hash
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
module ClassMethods
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
module DynaModel
|
2
|
+
module Query
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
# Failsafe
|
6
|
+
QUERY_TIMEOUT = 30 # seconds
|
7
|
+
DEFAULT_BATCH_SIZE = 100
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
|
11
|
+
def read_guid(guid, options={})
|
12
|
+
return nil if guid.blank?
|
13
|
+
if self.range_key
|
14
|
+
hash_value, range_value = guid.split(self.guid_delimiter)
|
15
|
+
self.read(hash_value, range_value, options)
|
16
|
+
else
|
17
|
+
self.read(guid, options)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def read(hash_value, range_value_or_options=nil, options=nil)
|
22
|
+
if self.range_key.nil?
|
23
|
+
item_attrs = self.dynamo_db_table.get_item(hash_value, range_value_or_options || {})[:item]
|
24
|
+
return nil if item_attrs.nil?
|
25
|
+
self.obj_from_attrs(Table.values_from_response_hash(item_attrs), (range_value_or_options || {}))
|
26
|
+
else
|
27
|
+
raise ArgumentError, "This table requires a range_key_value" if range_value_or_options.nil?
|
28
|
+
self.read_range(hash_value, (options || {}).merge(range: { self.range_key[:attribute_name].to_sym.eq => range_value_or_options})).first
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def read_multiple(keys, options={})
|
33
|
+
results_map = {}
|
34
|
+
results = self.dynamo_db_table.batch_get_item(keys, options)
|
35
|
+
results[:responses][self.dynamo_db_table_name(options[:shard_name])].each do |result|
|
36
|
+
attrs = Response.strip_attr_types(result)
|
37
|
+
obj = self.obj_from_attrs(attrs, options)
|
38
|
+
if self.dynamo_db_table.range_keys.present? && primary_range_key = self.dynamo_db_table.range_keys.find{|rk| rk[:primary_range_key] }
|
39
|
+
(results_map[attrs[self.dynamo_db_table.hash_key[:attribute_name]]] ||= {})[attrs[primary_range_key[:attribute_name]]] = obj
|
40
|
+
else
|
41
|
+
results_map[attrs[self.dynamo_db_table.hash_key[:attribute_name]]] = obj
|
42
|
+
end
|
43
|
+
end
|
44
|
+
results_map
|
45
|
+
end
|
46
|
+
|
47
|
+
# Read results up to the limit
|
48
|
+
# read_range("1", :range => { :varname.gte => "2"}, :limit => 10)
|
49
|
+
# Loop results in given batch size until limit is hit or no more results
|
50
|
+
# read_range("1", :range => { :varname.eq => "2"}, :batch => 10, :limit => 1000)
|
51
|
+
def read_range(hash_value, options={})
|
52
|
+
raise ArgumentError, "no range_key specified for this table" if self.dynamo_db_table.range_keys.blank? && self.global_secondary_indexes.blank?
|
53
|
+
aggregated_results = []
|
54
|
+
|
55
|
+
# Useful if doing pagination where you would need the last key evaluated
|
56
|
+
return_last_evaluated_key = options.delete(:return_last_evaluated_key)
|
57
|
+
batch_size = options.delete(:batch) || DEFAULT_BATCH_SIZE
|
58
|
+
max_results_limit = options[:limit]
|
59
|
+
if options[:limit] && options[:limit] > batch_size
|
60
|
+
options.merge!(:limit => batch_size)
|
61
|
+
end
|
62
|
+
|
63
|
+
results = self.dynamo_db_table.query(hash_value, options)
|
64
|
+
response = Response.new(results)
|
65
|
+
|
66
|
+
results[:member].each do |result|
|
67
|
+
attrs = Response.strip_attr_types(result)
|
68
|
+
aggregated_results << self.obj_from_attrs(attrs, options)
|
69
|
+
end
|
70
|
+
|
71
|
+
if response.more_results?
|
72
|
+
results_returned = response.count
|
73
|
+
batch_iteration = 0
|
74
|
+
Timeout::timeout(QUERY_TIMEOUT) do
|
75
|
+
while response.more_results?
|
76
|
+
if max_results_limit && (delta_results_limit = (max_results_limit-results_returned)) < batch_size
|
77
|
+
break if delta_results_limit == 0
|
78
|
+
options.merge!(limit: delta_results_limit)
|
79
|
+
else
|
80
|
+
options.merge!(limit: batch_size)
|
81
|
+
end
|
82
|
+
|
83
|
+
results = self.dynamo_db_table.query(hash_value, options.merge(exclusive_start_key: response.last_evaluated_key))
|
84
|
+
response = Response.new(results)
|
85
|
+
results[:member].each do |result|
|
86
|
+
attrs = Response.strip_attr_types(result)
|
87
|
+
aggregated_results << self.obj_from_attrs(attrs, options)
|
88
|
+
end
|
89
|
+
results_returned += response.count
|
90
|
+
batch_iteration += 1
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
if return_last_evaluated_key
|
96
|
+
{
|
97
|
+
last_evaluated_key: response.last_evaluated_key,
|
98
|
+
members: aggregated_results
|
99
|
+
}
|
100
|
+
else
|
101
|
+
aggregated_results
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def count_range(hash_value, options={})
|
106
|
+
raise ArgumentError, "no range_key specified for this table" if self.dynamo_db_table.range_keys.blank?
|
107
|
+
results = self.dynamo_db_table.query(hash_value, options.merge(select: :count))
|
108
|
+
Response.new(results).count
|
109
|
+
end
|
110
|
+
|
111
|
+
def read_first(hash_value, options={})
|
112
|
+
self.read_range(hash_value, options).first
|
113
|
+
end
|
114
|
+
|
115
|
+
#:count=>10, :scanned_count=>10, :last_evaluated_key=>{"guid"=>{:s=>"11f82550-5c5d-11e3-9b55-d311a43114ca"}}}
|
116
|
+
# :manual_batching => true|false
|
117
|
+
# return results with last_evaluated_key instead of automatically looping through (useful to throttle or )
|
118
|
+
def scan(options={})
|
119
|
+
aggregated_results = []
|
120
|
+
|
121
|
+
batch_size = options.delete(:batch) || DEFAULT_BATCH_SIZE
|
122
|
+
max_results_limit = options[:limit]
|
123
|
+
options[:limit] = batch_size
|
124
|
+
|
125
|
+
results = self.dynamo_db_table.scan(options)
|
126
|
+
response = Response.new(results)
|
127
|
+
|
128
|
+
results[:member].each do |result|
|
129
|
+
attrs = Response.strip_attr_types(result)
|
130
|
+
aggregated_results << self.obj_from_attrs(attrs, options)
|
131
|
+
end
|
132
|
+
|
133
|
+
if response.more_results? && !options[:manual_batching]
|
134
|
+
results_returned = response.count
|
135
|
+
batch_iteration = 0
|
136
|
+
Timeout::timeout(QUERY_TIMEOUT) do
|
137
|
+
while response.more_results?
|
138
|
+
if max_results_limit && (delta_results_limit = (max_results_limit-results_returned)) < batch_size
|
139
|
+
break if delta_results_limit == 0
|
140
|
+
options.merge!(limit: delta_results_limit)
|
141
|
+
else
|
142
|
+
options.merge!(limit: batch_size)
|
143
|
+
end
|
144
|
+
|
145
|
+
results = dynamo_table.scan(options.merge(exclusive_start_key: response.last_evaluated_key))
|
146
|
+
response = Response.new(results)
|
147
|
+
results[:member].each do |result|
|
148
|
+
attrs = Response.strip_attr_types(result)
|
149
|
+
aggregated_results << self.obj_from_attrs(attrs, options)
|
150
|
+
end
|
151
|
+
results_returned += response.count
|
152
|
+
batch_iteration += 1
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
if options[:manual_batching]
|
158
|
+
response_hash = {
|
159
|
+
results: aggregated_results,
|
160
|
+
last_evaluated_key: results[:last_evaluated_key]
|
161
|
+
}
|
162
|
+
response_hash.merge!(consumed_capacity: results[:consumed_capacity]) if results[:consumed_capacity]
|
163
|
+
response_hash
|
164
|
+
else
|
165
|
+
aggregated_results
|
166
|
+
end
|
167
|
+
end # scan
|
168
|
+
|
169
|
+
protected
|
170
|
+
def obj_from_attrs(attrs, options={})
|
171
|
+
obj = self.new(shard: self.shard_name(options[:shard_name]))
|
172
|
+
obj.send(:hydrate, nil, attrs)
|
173
|
+
if options[:select]
|
174
|
+
obj.instance_variable_set("@_select", options[:select])
|
175
|
+
if options[:select] != :all
|
176
|
+
#:all, :projected, :count, :specific
|
177
|
+
selected_attrs = []
|
178
|
+
# Primary hash/range key are always returned...
|
179
|
+
self.table_schema[:key_schema].each do |k|
|
180
|
+
selected_attrs << k[:attribute_name]
|
181
|
+
end
|
182
|
+
if options[:select] == :projected
|
183
|
+
index = ((self.table_schema[:global_secondary_indexes] || []) + (self.table_schema[:local_secondary_indexes] || [])).find { |i| i[:index_name] == options[:index_name].to_s }
|
184
|
+
raise "Index '#{options[:index_name]}' not found in table schema" unless index
|
185
|
+
index[:key_schema].each do |k|
|
186
|
+
selected_attrs << k[:attribute_name].to_s
|
187
|
+
end
|
188
|
+
if index[:projection] && index[:projection][:non_key_attributes]
|
189
|
+
index[:projection][:non_key_attributes].each do |a|
|
190
|
+
selected_attrs << a.to_s
|
191
|
+
end
|
192
|
+
end
|
193
|
+
elsif options[:select].is_a?(Array)
|
194
|
+
obj.instance_variable_set("@_select", :specific)
|
195
|
+
selected_attrs += options[:select].map(&:to_s)
|
196
|
+
end
|
197
|
+
selected_attrs.uniq!
|
198
|
+
obj.instance_variable_set("@_selected_attributes", selected_attrs.compact)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
obj
|
202
|
+
end
|
203
|
+
|
204
|
+
end # ClassMethods
|
205
|
+
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module DynaModel
|
2
|
+
class Response
|
3
|
+
|
4
|
+
def initialize(response)
|
5
|
+
raise ArgumentError, "response should be an AWS::Core::Response" unless response.is_a?(AWS::Core::Response)
|
6
|
+
@raw_response = response
|
7
|
+
end
|
8
|
+
|
9
|
+
#def values_from_response_hash(options = {})
|
10
|
+
#@raw_response.inject({}) do |h, (key, value_hash)|
|
11
|
+
#h.update(key => value_hash.to_a.last)
|
12
|
+
#end
|
13
|
+
#end
|
14
|
+
|
15
|
+
def count
|
16
|
+
@raw_response[:count]
|
17
|
+
end
|
18
|
+
|
19
|
+
def last_evaluated_key
|
20
|
+
@raw_response[:last_evaluated_key]
|
21
|
+
end
|
22
|
+
|
23
|
+
def more_results?
|
24
|
+
@raw_response.has_key?(:last_evaluated_key)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.strip_attr_types(hash)
|
28
|
+
attrs = {}
|
29
|
+
hash.each_pair do |k,v|
|
30
|
+
attrs[k] = v.values.first
|
31
|
+
end
|
32
|
+
attrs
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,251 @@
|
|
1
|
+
module DynaModel
|
2
|
+
module Schema
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
|
7
|
+
KEY_TYPE = {
|
8
|
+
hash: "HASH",
|
9
|
+
range: "RANGE"
|
10
|
+
}
|
11
|
+
|
12
|
+
PROJECTION_TYPE = {
|
13
|
+
keys_only: "KEYS_ONLY",
|
14
|
+
all: "ALL",
|
15
|
+
include: "INCLUDE"
|
16
|
+
}
|
17
|
+
|
18
|
+
ATTR_TYPES = {
|
19
|
+
AWS::Record::Attributes::StringAttr => "S",
|
20
|
+
AWS::Record::Attributes::IntegerAttr => "N",
|
21
|
+
AWS::Record::Attributes::FloatAttr => "N",
|
22
|
+
AWS::Record::Attributes::BooleanAttr => "S",
|
23
|
+
AWS::Record::Attributes::DateTimeAttr => "N",
|
24
|
+
AWS::Record::Attributes::DateAttr => "N",
|
25
|
+
AWS::Record::Attributes::SerializedAttr => "B"
|
26
|
+
}
|
27
|
+
|
28
|
+
def table_schema
|
29
|
+
schema = {
|
30
|
+
table_name: dynamo_db_table_name,
|
31
|
+
provisioned_throughput: {
|
32
|
+
read_capacity_units: read_provision,
|
33
|
+
write_capacity_units: write_provision
|
34
|
+
},
|
35
|
+
key_schema: key_schema,
|
36
|
+
attribute_definitions: attribute_definitions
|
37
|
+
}
|
38
|
+
schema[:local_secondary_indexes] = local_secondary_indexes unless local_secondary_indexes.blank?
|
39
|
+
schema[:global_secondary_indexes] = global_secondary_indexes unless global_secondary_indexes.blank?
|
40
|
+
schema
|
41
|
+
end
|
42
|
+
|
43
|
+
def guid_delimiter(val=nil)
|
44
|
+
if val
|
45
|
+
raise(ArgumentError, "Invalid guid_delimiter") if val.blank?
|
46
|
+
@guid_delimiter = val.to_s
|
47
|
+
else
|
48
|
+
@guid_delimiter || DynaModel::Config.default_guid_delimiter
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def read_provision(val=nil)
|
53
|
+
if val
|
54
|
+
raise(ArgumentError, "Invalid read provision") unless val.to_i >= 1
|
55
|
+
@dynamo_read_provision = val.to_i
|
56
|
+
else
|
57
|
+
@dynamo_read_provision || DynaModel::Config.read_provision
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def write_provision(val=nil)
|
62
|
+
if val
|
63
|
+
raise(ArgumentError, "Invalid write provision") unless val.to_i >= 1
|
64
|
+
@dynamo_write_provision = val.to_i
|
65
|
+
else
|
66
|
+
@dynamo_write_provision || DynaModel::Config.write_provision
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def hash_key(hash_key_key=nil)
|
71
|
+
if hash_key_key
|
72
|
+
hash_key_attribute = self.attributes[hash_key_key.to_s]
|
73
|
+
raise(ArgumentError, "Could not find attribute definition for hash_key #{hash_key_key}") unless hash_key_attribute
|
74
|
+
raise(ArgumentError, "Invalid attribute type for hash_key") unless [AWS::Record::Attributes::StringAttr, AWS::Record::Attributes::IntegerAttr, AWS::Record::Attributes::FloatAttr].include?(hash_key_attribute.class)
|
75
|
+
|
76
|
+
validates_presence_of hash_key_attribute.name.to_sym
|
77
|
+
|
78
|
+
@dynamo_hash_key = {
|
79
|
+
attribute_name: hash_key_attribute.name,
|
80
|
+
key_type: KEY_TYPE[:hash]
|
81
|
+
}
|
82
|
+
else
|
83
|
+
@dynamo_hash_key
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def range_key(range_key_key=nil)
|
88
|
+
if range_key_key
|
89
|
+
range_key_attribute = self.attributes[range_key_key.to_s]
|
90
|
+
raise(ArgumentError, "Could not find attribute definition for range_key #{range_key_key}") unless range_key_attribute
|
91
|
+
raise(ArgumentError, "Invalid attribute type for range_key") unless [AWS::Record::Attributes::StringAttr, AWS::Record::Attributes::IntegerAttr, AWS::Record::Attributes::FloatAttr].include?(range_key_attribute.class)
|
92
|
+
|
93
|
+
validates_presence_of range_key_attribute.name.to_sym
|
94
|
+
|
95
|
+
@dynamo_range_key = {
|
96
|
+
attribute_name: range_key_attribute.name,
|
97
|
+
key_type: KEY_TYPE[:range]
|
98
|
+
}
|
99
|
+
else
|
100
|
+
@dynamo_range_key
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# TODO - need to add projections?
|
105
|
+
def attribute_definitions
|
106
|
+
# Keys for hash/range/secondary
|
107
|
+
# S | N | B
|
108
|
+
|
109
|
+
keys = []
|
110
|
+
keys << hash_key[:attribute_name]
|
111
|
+
keys << range_key[:attribute_name] if range_key
|
112
|
+
local_secondary_indexes.each do |lsi|
|
113
|
+
keys << lsi[:key_schema].select{|h| h[:key_type] == "RANGE"}.first[:attribute_name]
|
114
|
+
end
|
115
|
+
|
116
|
+
global_secondary_indexes.each do |lsi|
|
117
|
+
lsi[:key_schema].each do |a|
|
118
|
+
keys << a[:attribute_name]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
definitions = keys.uniq.collect do |k|
|
123
|
+
attr = self.attributes[k.to_s]
|
124
|
+
{
|
125
|
+
attribute_name: attr.name,
|
126
|
+
attribute_type: attribute_type_indicator(attr)
|
127
|
+
}
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def attribute_type_indicator(attr)
|
132
|
+
if attr_type = ATTR_TYPES[attr.class]
|
133
|
+
attr_type
|
134
|
+
else
|
135
|
+
raise "unsupported attribute type #{attr.class}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def key_schema
|
140
|
+
raise(ArgumentError, 'hash_key was not set for this table') if @dynamo_hash_key.blank?
|
141
|
+
schema = [hash_key]
|
142
|
+
schema << range_key if range_key
|
143
|
+
schema
|
144
|
+
end
|
145
|
+
|
146
|
+
def global_secondary_indexes
|
147
|
+
@global_secondary_indexes ||= []
|
148
|
+
end
|
149
|
+
|
150
|
+
# { hash_key: :hash_key_here, range_key: :optional_range_key_here }
|
151
|
+
# :name
|
152
|
+
# :projection
|
153
|
+
# :read_provision
|
154
|
+
# :write_provision
|
155
|
+
def global_secondary_index(index_name, options={})
|
156
|
+
options[:projection] ||= :keys_only
|
157
|
+
global_secondary_index_hash = {
|
158
|
+
projection: {},
|
159
|
+
provisioned_throughput: {
|
160
|
+
read_capacity_units: options[:read_provision] || read_provision,
|
161
|
+
write_capacity_units: options[:write_provision] || write_provision
|
162
|
+
}
|
163
|
+
}
|
164
|
+
if options[:projection].is_a?(Array) && options[:projection].size > 0
|
165
|
+
options[:projection].each do |non_key_attr|
|
166
|
+
attr = self.attributes[non_key_attr.to_s]
|
167
|
+
raise(ArgumentError, "Could not find attribute definition for projection on #{non_key_attr}") unless attr
|
168
|
+
(global_secondary_index_hash[:projection][:non_key_attributes] ||= []) << attr.name
|
169
|
+
end
|
170
|
+
global_secondary_index_hash[:projection][:projection_type] = PROJECTION_TYPE[:include]
|
171
|
+
else
|
172
|
+
raise(ArgumentError, 'projection must be :all, :keys_only, Array (or attrs)') unless options[:projection] == :keys_only || options[:projection] == :all
|
173
|
+
global_secondary_index_hash[:projection][:projection_type] = PROJECTION_TYPE[options[:projection]]
|
174
|
+
end
|
175
|
+
|
176
|
+
if !options.has_key?(:hash_key) || self.attributes[options[:hash_key].to_s].blank?
|
177
|
+
raise(ArgumentError, "Could not find attribute definition for global secondary index on hash_key specified")
|
178
|
+
end
|
179
|
+
hash_key_attr = self.attributes[options[:hash_key].to_s]
|
180
|
+
|
181
|
+
if options.has_key?(:range_key) && self.attributes[options[:range_key].to_s].blank?
|
182
|
+
raise(ArgumentError, "Could not find attribute definition for global secondary index on range_key specified")
|
183
|
+
end
|
184
|
+
range_key_attr = nil
|
185
|
+
range_key_attr = self.attributes[options[:range_key].to_s] if options.has_key?(:range_key)
|
186
|
+
|
187
|
+
## Force naming of index_name for lookup later
|
188
|
+
#global_secondary_index_hash[:index_name] = (index_name.to_s || "#{hash_key_attr.name}#{"_#{range_key_attr.name}" if range_key_attr}_gsi_index".camelcase)
|
189
|
+
global_secondary_index_hash[:index_name] = index_name.to_s
|
190
|
+
|
191
|
+
global_secondary_index_hash[:key_schema] = [
|
192
|
+
{
|
193
|
+
attribute_name: hash_key_attr.name,
|
194
|
+
key_type: KEY_TYPE[:hash]
|
195
|
+
}
|
196
|
+
]
|
197
|
+
global_secondary_index_hash[:key_schema] << {
|
198
|
+
attribute_name: range_key_attr.name,
|
199
|
+
key_type: KEY_TYPE[:range]
|
200
|
+
} if range_key_attr
|
201
|
+
|
202
|
+
return false if (@global_secondary_indexes ||= []).select {|i| i[:index_name] == global_secondary_index_hash[:index_name] }.present? # Do not add if we already have a range key set for this attr
|
203
|
+
(@global_secondary_indexes ||= []) << global_secondary_index_hash
|
204
|
+
end
|
205
|
+
|
206
|
+
def local_secondary_indexes
|
207
|
+
@local_secondary_indexes ||= []
|
208
|
+
end
|
209
|
+
|
210
|
+
def local_secondary_index(range_key_attr, options={})
|
211
|
+
options[:projection] ||= :keys_only
|
212
|
+
local_secondary_index_hash = {
|
213
|
+
projection: {}
|
214
|
+
}
|
215
|
+
if options[:projection].is_a?(Array) && options[:projection].size > 0
|
216
|
+
options[:projection].each do |non_key_attr|
|
217
|
+
attr = self.attributes[non_key_attr.to_s]
|
218
|
+
raise(ArgumentError, "Could not find attribute definition for projection on #{non_key_attr}") unless attr
|
219
|
+
(local_secondary_index_hash[:projection][:non_key_attributes] ||= []) << attr.name
|
220
|
+
end
|
221
|
+
local_secondary_index_hash[:projection][:projection_type] = PROJECTION_TYPE[:include]
|
222
|
+
else
|
223
|
+
raise(ArgumentError, 'projection must be :all, :keys_only, Array (or attrs)') unless options[:projection] == :keys_only || options[:projection] == :all
|
224
|
+
local_secondary_index_hash[:projection][:projection_type] = PROJECTION_TYPE[options[:projection]]
|
225
|
+
end
|
226
|
+
|
227
|
+
range_attr = self.attributes[range_key_attr.to_s]
|
228
|
+
raise(ArgumentError, "Could not find attribute definition for local secondary index on #{range_key_attr}") unless range_attr
|
229
|
+
local_secondary_index_hash[:index_name] = (options[:name] || options[:index_name] || "#{range_attr.name}_index".camelcase)
|
230
|
+
|
231
|
+
hash_key_attr = self.attributes[hash_key[:attribute_name].to_s]
|
232
|
+
raise(ArgumentError, "Could not find attribute definition for hash_key") unless hash_key_attr
|
233
|
+
|
234
|
+
local_secondary_index_hash[:key_schema] = [
|
235
|
+
{
|
236
|
+
attribute_name: hash_key_attr.name,
|
237
|
+
key_type: KEY_TYPE[:hash]
|
238
|
+
},
|
239
|
+
{
|
240
|
+
attribute_name: range_attr.name,
|
241
|
+
key_type: KEY_TYPE[:range]
|
242
|
+
}
|
243
|
+
]
|
244
|
+
return false if (@local_secondary_indexes ||= []).select {|i| i[:index_name] == local_secondary_index_hash[:index_name] }.present? # Do not add if we already have a range key set for this attr
|
245
|
+
(@local_secondary_indexes ||= []) << local_secondary_index_hash
|
246
|
+
end
|
247
|
+
|
248
|
+
end # ClassMethods
|
249
|
+
|
250
|
+
end
|
251
|
+
end
|