dynamoid-edge 1.1.0
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/CHANGELOG.md +21 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +377 -0
- data/Rakefile +67 -0
- data/dynamoid-edge.gemspec +74 -0
- data/lib/dynamoid/adapter.rb +181 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v2.rb +761 -0
- data/lib/dynamoid/associations/association.rb +105 -0
- data/lib/dynamoid/associations/belongs_to.rb +44 -0
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +40 -0
- data/lib/dynamoid/associations/has_many.rb +39 -0
- data/lib/dynamoid/associations/has_one.rb +39 -0
- data/lib/dynamoid/associations/many_association.rb +191 -0
- data/lib/dynamoid/associations/single_association.rb +69 -0
- data/lib/dynamoid/associations.rb +106 -0
- data/lib/dynamoid/components.rb +37 -0
- data/lib/dynamoid/config/options.rb +78 -0
- data/lib/dynamoid/config.rb +54 -0
- data/lib/dynamoid/criteria/chain.rb +212 -0
- data/lib/dynamoid/criteria.rb +29 -0
- data/lib/dynamoid/dirty.rb +47 -0
- data/lib/dynamoid/document.rb +201 -0
- data/lib/dynamoid/errors.rb +63 -0
- data/lib/dynamoid/fields.rb +156 -0
- data/lib/dynamoid/finders.rb +197 -0
- data/lib/dynamoid/identity_map.rb +92 -0
- data/lib/dynamoid/middleware/identity_map.rb +16 -0
- data/lib/dynamoid/persistence.rb +324 -0
- data/lib/dynamoid/validations.rb +36 -0
- data/lib/dynamoid.rb +50 -0
- metadata +226 -0
@@ -0,0 +1,761 @@
|
|
1
|
+
module Dynamoid
|
2
|
+
module AdapterPlugin
|
3
|
+
|
4
|
+
# The AwsSdkV2 adapter provides support for the aws-sdk version 2 for ruby.
|
5
|
+
class AwsSdkV2
|
6
|
+
attr_reader :table_cache
|
7
|
+
|
8
|
+
# Establish the connection to DynamoDB.
|
9
|
+
#
|
10
|
+
# @return [Aws::DynamoDB::Client] the DynamoDB connection
|
11
|
+
def connect!
|
12
|
+
@client = if Dynamoid::Config.endpoint?
|
13
|
+
Aws::DynamoDB::Client.new(endpoint: Dynamoid::Config.endpoint)
|
14
|
+
else
|
15
|
+
Aws::DynamoDB::Client.new
|
16
|
+
end
|
17
|
+
@table_cache = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
# Return the client object.
|
21
|
+
#
|
22
|
+
# @since 1.0.0
|
23
|
+
def client
|
24
|
+
@client
|
25
|
+
end
|
26
|
+
|
27
|
+
# Get many items at once from DynamoDB. More efficient than getting each item individually.
|
28
|
+
#
|
29
|
+
# @example Retrieve IDs 1 and 2 from the table testtable
|
30
|
+
# Dynamoid::Adapter::AwsSdkV2.batch_get_item({'table1' => ['1', '2']})
|
31
|
+
#
|
32
|
+
# @param [Hash] table_ids the hash of tables and IDs to retrieve
|
33
|
+
# @param [Hash] options to be passed to underlying BatchGet call
|
34
|
+
#
|
35
|
+
# @return [Hash] a hash where keys are the table names and the values are the retrieved items
|
36
|
+
#
|
37
|
+
# @since 1.0.0
|
38
|
+
#
|
39
|
+
# @todo: Provide support for passing options to underlying batch_get_item http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_get_item-instance_method
|
40
|
+
def batch_get_item(table_ids, options = {})
|
41
|
+
request_items = Hash.new{|h, k| h[k] = []}
|
42
|
+
return request_items if table_ids.all?{|k, v| v.empty?}
|
43
|
+
|
44
|
+
table_ids.each do |t, ids|
|
45
|
+
next if ids.empty?
|
46
|
+
tbl = describe_table(t)
|
47
|
+
hk = tbl.hash_key.to_s
|
48
|
+
rng = tbl.range_key.to_s
|
49
|
+
|
50
|
+
keys = if rng.present?
|
51
|
+
Array(ids).map do |h,r|
|
52
|
+
{ hk => h, rng => r }
|
53
|
+
end
|
54
|
+
else
|
55
|
+
Array(ids).map do |id|
|
56
|
+
{ hk => id }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
request_items[t] = {
|
61
|
+
keys: keys
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
results = client.batch_get_item(
|
66
|
+
request_items: request_items
|
67
|
+
)
|
68
|
+
|
69
|
+
ret = Hash.new([].freeze) # Default for tables where no rows are returned
|
70
|
+
results.data[:responses].each do |table, rows|
|
71
|
+
ret[table] = rows.collect { |r| result_item_to_hash(r) }
|
72
|
+
end
|
73
|
+
ret
|
74
|
+
end
|
75
|
+
|
76
|
+
# Delete many items at once from DynamoDB. More efficient than delete each item individually.
|
77
|
+
#
|
78
|
+
# @example Delete IDs 1 and 2 from the table testtable
|
79
|
+
# Dynamoid::Adapter::AwsSdk.batch_delete_item('table1' => ['1', '2'])
|
80
|
+
#or
|
81
|
+
# Dynamoid::Adapter::AwsSdkV2.batch_delete_item('table1' => [['hk1', 'rk2'], ['hk1', 'rk2']]]))
|
82
|
+
#
|
83
|
+
# @param [Hash] options the hash of tables and IDs to delete
|
84
|
+
#
|
85
|
+
# @return nil
|
86
|
+
#
|
87
|
+
# @todo: Provide support for passing options to underlying delete_item http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#delete_item-instance_method
|
88
|
+
def batch_delete_item(options)
|
89
|
+
options.each_pair do |table_name, ids|
|
90
|
+
table = describe_table(table_name)
|
91
|
+
ids.each do |id|
|
92
|
+
client.delete_item(table_name: table_name, key: key_stanza(table, *id))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
nil
|
96
|
+
end
|
97
|
+
|
98
|
+
# Create a table on DynamoDB. This usually takes a long time to complete.
|
99
|
+
#
|
100
|
+
# @param [String] table_name the name of the table to create
|
101
|
+
# @param [Symbol] key the table's primary key (defaults to :id)
|
102
|
+
# @param [Hash] options provide a range key here if the table has a composite key
|
103
|
+
# @option options [Array<Dynamoid::Indexes::Index>] local_secondary_indexes
|
104
|
+
# @option options [Array<Dynamoid::Indexes::Index>] global_secondary_indexes
|
105
|
+
# @option options [Symbol] hash_key_type The type of the hash key
|
106
|
+
# @since 1.0.0
|
107
|
+
def create_table(table_name, key = :id, options = {})
|
108
|
+
Dynamoid.logger.info "Creating #{table_name} table. This could take a while."
|
109
|
+
read_capacity = options[:read_capacity] || Dynamoid::Config.read_capacity
|
110
|
+
write_capacity = options[:write_capacity] || Dynamoid::Config.write_capacity
|
111
|
+
|
112
|
+
secondary_indexes = options.slice(
|
113
|
+
:local_secondary_indexes,
|
114
|
+
:global_secondary_indexes
|
115
|
+
)
|
116
|
+
ls_indexes = options[:local_secondary_indexes]
|
117
|
+
gs_indexes = options[:global_secondary_indexes]
|
118
|
+
|
119
|
+
key_schema = {
|
120
|
+
:hash_key_schema => { key => (options[:hash_key_type] || :string) },
|
121
|
+
:range_key_schema => options[:range_key]
|
122
|
+
}
|
123
|
+
attribute_definitions = build_all_attribute_definitions(
|
124
|
+
key_schema,
|
125
|
+
secondary_indexes
|
126
|
+
)
|
127
|
+
key_schema = aws_key_schema(
|
128
|
+
key_schema[:hash_key_schema],
|
129
|
+
key_schema[:range_key_schema]
|
130
|
+
)
|
131
|
+
|
132
|
+
client_opts = {
|
133
|
+
table_name: table_name,
|
134
|
+
provisioned_throughput: {
|
135
|
+
read_capacity_units: read_capacity,
|
136
|
+
write_capacity_units: write_capacity
|
137
|
+
},
|
138
|
+
key_schema: key_schema,
|
139
|
+
attribute_definitions: attribute_definitions
|
140
|
+
}
|
141
|
+
|
142
|
+
if ls_indexes.present?
|
143
|
+
client_opts[:local_secondary_indexes] = ls_indexes.map do |index|
|
144
|
+
index_to_aws_hash(index)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
if gs_indexes.present?
|
149
|
+
client_opts[:global_secondary_indexes] = gs_indexes.map do |index|
|
150
|
+
index_to_aws_hash(index)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
client.create_table(client_opts)
|
154
|
+
rescue Aws::DynamoDB::Errors::ResourceInUseException => e
|
155
|
+
Dynamoid.logger.error "Table #{table_name} cannot be created as it already exists"
|
156
|
+
end
|
157
|
+
|
158
|
+
# Removes an item from DynamoDB.
|
159
|
+
#
|
160
|
+
# @param [String] table_name the name of the table
|
161
|
+
# @param [String] key the hash key of the item to delete
|
162
|
+
# @param [Hash] options provide a range key here if the table has a composite key
|
163
|
+
#
|
164
|
+
# @since 1.0.0
|
165
|
+
#
|
166
|
+
# @todo: Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#delete_item-instance_method
|
167
|
+
def delete_item(table_name, key, options = {})
|
168
|
+
range_key = options[:range_key]
|
169
|
+
conditions = options[:conditions]
|
170
|
+
table = describe_table(table_name)
|
171
|
+
client.delete_item(
|
172
|
+
table_name: table_name,
|
173
|
+
key: key_stanza(table, key, range_key),
|
174
|
+
expected: expected_stanza(conditions)
|
175
|
+
)
|
176
|
+
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
|
177
|
+
raise Dynamoid::Errors::ConditionalCheckFailedException, e
|
178
|
+
end
|
179
|
+
|
180
|
+
# Deletes an entire table from DynamoDB.
|
181
|
+
#
|
182
|
+
# @param [String] table_name the name of the table to destroy
|
183
|
+
#
|
184
|
+
# @since 1.0.0
|
185
|
+
def delete_table(table_name)
|
186
|
+
client.delete_table(table_name: table_name)
|
187
|
+
table_cache.clear
|
188
|
+
end
|
189
|
+
|
190
|
+
# @todo Add a DescribeTable method.
|
191
|
+
|
192
|
+
# Fetches an item from DynamoDB.
|
193
|
+
#
|
194
|
+
# @param [String] table_name the name of the table
|
195
|
+
# @param [String] key the hash key of the item to find
|
196
|
+
# @param [Hash] options provide a range key here if the table has a composite key
|
197
|
+
#
|
198
|
+
# @return [Hash] a hash representing the raw item in DynamoDB
|
199
|
+
#
|
200
|
+
# @since 1.0.0
|
201
|
+
#
|
202
|
+
# @todo Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#get_item-instance_method
|
203
|
+
def get_item(table_name, key, options = {})
|
204
|
+
table = describe_table(table_name)
|
205
|
+
range_key = options.delete(:range_key)
|
206
|
+
|
207
|
+
item = client.get_item(table_name: table_name,
|
208
|
+
key: key_stanza(table, key, range_key)
|
209
|
+
)[:item]
|
210
|
+
item ? result_item_to_hash(item) : nil
|
211
|
+
end
|
212
|
+
|
213
|
+
# Edits an existing item's attributes, or adds a new item to the table if it does not already exist. You can put, delete, or add attribute values
|
214
|
+
#
|
215
|
+
# @param [String] table_name the name of the table
|
216
|
+
# @param [String] key the hash key of the item to find
|
217
|
+
# @param [Hash] options provide a range key here if the table has a composite key
|
218
|
+
#
|
219
|
+
# @return new attributes for the record
|
220
|
+
#
|
221
|
+
# @todo Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#update_item-instance_method
|
222
|
+
def update_item(table_name, key, options = {})
|
223
|
+
range_key = options.delete(:range_key)
|
224
|
+
conditions = options.delete(:conditions)
|
225
|
+
table = describe_table(table_name)
|
226
|
+
|
227
|
+
yield(iu = ItemUpdater.new(table, key, range_key))
|
228
|
+
|
229
|
+
raise "non-empty options: #{options}" unless options.empty?
|
230
|
+
begin
|
231
|
+
result = client.update_item(table_name: table_name,
|
232
|
+
key: key_stanza(table, key, range_key),
|
233
|
+
attribute_updates: iu.to_h,
|
234
|
+
expected: expected_stanza(conditions),
|
235
|
+
return_values: "ALL_NEW"
|
236
|
+
)
|
237
|
+
result_item_to_hash(result[:attributes])
|
238
|
+
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
|
239
|
+
raise Dynamoid::Errors::ConditionalCheckFailedException, e
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# List all tables on DynamoDB.
|
244
|
+
#
|
245
|
+
# @since 1.0.0
|
246
|
+
#
|
247
|
+
# @todo Provide limit support http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#update_item-instance_method
|
248
|
+
def list_tables
|
249
|
+
client.list_tables[:table_names]
|
250
|
+
end
|
251
|
+
|
252
|
+
# Persists an item on DynamoDB.
|
253
|
+
#
|
254
|
+
# @param [String] table_name the name of the table
|
255
|
+
# @param [Object] object a hash or Dynamoid object to persist
|
256
|
+
#
|
257
|
+
# @since 1.0.0
|
258
|
+
#
|
259
|
+
# @todo: Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#put_item-instance_method
|
260
|
+
def put_item(table_name, object, options = nil)
|
261
|
+
item = {}
|
262
|
+
|
263
|
+
object.each do |k, v|
|
264
|
+
next if v.nil? || (v.respond_to?(:empty?) && v.empty?)
|
265
|
+
item[k.to_s] = v
|
266
|
+
end
|
267
|
+
|
268
|
+
begin
|
269
|
+
client.put_item(table_name: table_name,
|
270
|
+
item: item,
|
271
|
+
expected: expected_stanza(options)
|
272
|
+
)
|
273
|
+
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
|
274
|
+
raise Dynamoid::Errors::ConditionalCheckFailedException, e
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
# Query the DynamoDB table. This employs DynamoDB's indexes so is generally faster than scanning, but is
|
279
|
+
# only really useful for range queries, since it can only find by one hash key at once. Only provide
|
280
|
+
# one range key to the hash.
|
281
|
+
#
|
282
|
+
# @param [String] table_name the name of the table
|
283
|
+
# @param [Hash] opts the options to query the table with
|
284
|
+
# @option opts [String] :hash_value the value of the hash key to find
|
285
|
+
# @option opts [Number, Number] :range_between find the range key within this range
|
286
|
+
# @option opts [Number] :range_greater_than find range keys greater than this
|
287
|
+
# @option opts [Number] :range_less_than find range keys less than this
|
288
|
+
# @option opts [Number] :range_gte find range keys greater than or equal to this
|
289
|
+
# @option opts [Number] :range_lte find range keys less than or equal to this
|
290
|
+
#
|
291
|
+
# @return [Enumerable] matching items
|
292
|
+
#
|
293
|
+
# @since 1.0.0
|
294
|
+
#
|
295
|
+
# @todo Provide support for various other options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#query-instance_method
|
296
|
+
def query(table_name, opts = {})
|
297
|
+
table = describe_table(table_name)
|
298
|
+
hk = (opts[:hash_key].present? ? opts[:hash_key] : table.hash_key).to_s
|
299
|
+
rng = (opts[:range_key].present? ? opts[:range_key] : table.range_key).to_s
|
300
|
+
q = opts.slice(
|
301
|
+
:consistent_read,
|
302
|
+
:scan_index_forward,
|
303
|
+
:limit,
|
304
|
+
:select,
|
305
|
+
:index_name
|
306
|
+
)
|
307
|
+
|
308
|
+
opts.delete(:consistent_read)
|
309
|
+
opts.delete(:scan_index_forward)
|
310
|
+
opts.delete(:limit)
|
311
|
+
opts.delete(:select)
|
312
|
+
opts.delete(:index_name)
|
313
|
+
|
314
|
+
opts.delete(:next_token).tap do |token|
|
315
|
+
break unless token
|
316
|
+
q[:exclusive_start_key] = {
|
317
|
+
hk => token[:hash_key_element],
|
318
|
+
rng => token[:range_key_element]
|
319
|
+
}
|
320
|
+
end
|
321
|
+
|
322
|
+
key_conditions = {
|
323
|
+
hk => {
|
324
|
+
# TODO: Provide option for other operators like NE, IN, LE, etc
|
325
|
+
comparison_operator: EQ,
|
326
|
+
attribute_value_list: [
|
327
|
+
opts.delete(:hash_value).freeze
|
328
|
+
]
|
329
|
+
}
|
330
|
+
}
|
331
|
+
|
332
|
+
opts.each_pair do |k, v|
|
333
|
+
# TODO: ATM, only few comparison operators are supported, provide support for all operators
|
334
|
+
next unless(op = RANGE_MAP[k])
|
335
|
+
key_conditions[rng] = {
|
336
|
+
comparison_operator: op,
|
337
|
+
attribute_value_list: [
|
338
|
+
opts.delete(k).freeze
|
339
|
+
].flatten # Flatten as BETWEEN operator specifies array of two elements
|
340
|
+
}
|
341
|
+
end
|
342
|
+
|
343
|
+
q[:table_name] = table_name
|
344
|
+
q[:key_conditions] = key_conditions
|
345
|
+
|
346
|
+
Enumerator.new { |y|
|
347
|
+
result = client.query(q)
|
348
|
+
|
349
|
+
result.items.each { |r|
|
350
|
+
y << result_item_to_hash(r)
|
351
|
+
}
|
352
|
+
}
|
353
|
+
end
|
354
|
+
|
355
|
+
EQ = "EQ".freeze
|
356
|
+
|
357
|
+
RANGE_MAP = {
|
358
|
+
range_greater_than: 'GT',
|
359
|
+
range_less_than: 'LT',
|
360
|
+
range_gte: 'GE',
|
361
|
+
range_lte: 'LE',
|
362
|
+
range_begins_with: 'BEGINS_WITH',
|
363
|
+
range_between: 'BETWEEN',
|
364
|
+
range_eq: 'EQ'
|
365
|
+
}
|
366
|
+
|
367
|
+
# Scan the DynamoDB table. This is usually a very slow operation as it naively filters all data on
|
368
|
+
# the DynamoDB servers.
|
369
|
+
#
|
370
|
+
# @param [String] table_name the name of the table
|
371
|
+
# @param [Hash] scan_hash a hash of attributes: matching records will be returned by the scan
|
372
|
+
#
|
373
|
+
# @return [Enumerable] matching items
|
374
|
+
#
|
375
|
+
# @since 1.0.0
|
376
|
+
#
|
377
|
+
# @todo: Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#scan-instance_method
|
378
|
+
def scan(table_name, scan_hash, select_opts = {})
|
379
|
+
limit = select_opts.delete(:limit)
|
380
|
+
batch = select_opts.delete(:batch_size)
|
381
|
+
|
382
|
+
request = { table_name: table_name }
|
383
|
+
request[:limit] = batch || limit if batch || limit
|
384
|
+
request[:scan_filter] = scan_hash.reduce({}) do |memo, kvp|
|
385
|
+
memo[kvp[0].to_s] = {
|
386
|
+
attribute_value_list: [kvp[1]],
|
387
|
+
# TODO: Provide support for all comparison operators
|
388
|
+
comparison_operator: EQ
|
389
|
+
}
|
390
|
+
memo
|
391
|
+
end if scan_hash.present?
|
392
|
+
|
393
|
+
Enumerator.new do |y|
|
394
|
+
# Batch loop, pulls multiple requests until done using the start_key
|
395
|
+
loop do
|
396
|
+
results = client.scan(request)
|
397
|
+
|
398
|
+
results.data[:items].each { |row| y << result_item_to_hash(row) }
|
399
|
+
|
400
|
+
if((lk = results[:last_evaluated_key]) && batch)
|
401
|
+
request[:exclusive_start_key] = lk
|
402
|
+
else
|
403
|
+
break
|
404
|
+
end
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
|
410
|
+
#
|
411
|
+
# Truncates all records in the given table
|
412
|
+
#
|
413
|
+
# @param [String] table_name the name of the table
|
414
|
+
#
|
415
|
+
# @since 1.0.0
|
416
|
+
def truncate(table_name)
|
417
|
+
table = describe_table(table_name)
|
418
|
+
hk = table.hash_key
|
419
|
+
rk = table.range_key
|
420
|
+
|
421
|
+
scan(table_name, {}, {}).each do |attributes|
|
422
|
+
opts = {range_key: attributes[rk.to_sym] } if rk
|
423
|
+
delete_item(table_name, attributes[hk], opts)
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
def count(table_name)
|
428
|
+
describe_table(table_name, true).item_count
|
429
|
+
end
|
430
|
+
|
431
|
+
protected
|
432
|
+
|
433
|
+
STRING_TYPE = "S".freeze
|
434
|
+
NUM_TYPE = "N".freeze
|
435
|
+
BINARY_TYPE = "B".freeze
|
436
|
+
|
437
|
+
#Converts from symbol to the API string for the given data type
|
438
|
+
# E.g. :number -> 'N'
|
439
|
+
def api_type(type)
|
440
|
+
case(type)
|
441
|
+
when :string then STRING_TYPE
|
442
|
+
when :number then NUM_TYPE
|
443
|
+
when :binary then BINARY_TYPE
|
444
|
+
else raise "Unknown type: #{type}"
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
#
|
449
|
+
# The key hash passed on get_item, put_item, delete_item, update_item, etc
|
450
|
+
#
|
451
|
+
def key_stanza(table, hash_key, range_key = nil)
|
452
|
+
key = { table.hash_key.to_s => hash_key }
|
453
|
+
key[table.range_key.to_s] = range_key if range_key
|
454
|
+
key
|
455
|
+
end
|
456
|
+
|
457
|
+
#
|
458
|
+
# @param [Hash] conditions Conditions to enforce on operation (e.g. { :if => { :count => 5 }, :unless_exists => ['id']})
|
459
|
+
# @return an Expected stanza for the given conditions hash
|
460
|
+
#
|
461
|
+
def expected_stanza(conditions = nil)
|
462
|
+
expected = Hash.new { |h,k| h[k] = {} }
|
463
|
+
return expected unless conditions
|
464
|
+
|
465
|
+
conditions[:unless_exists].try(:each) do |col|
|
466
|
+
expected[col.to_s][:exists] = false
|
467
|
+
end
|
468
|
+
conditions[:if].try(:each) do |col,val|
|
469
|
+
expected[col.to_s][:value] = val
|
470
|
+
end
|
471
|
+
|
472
|
+
expected
|
473
|
+
end
|
474
|
+
|
475
|
+
HASH_KEY = "HASH".freeze
|
476
|
+
RANGE_KEY = "RANGE".freeze
|
477
|
+
|
478
|
+
#
|
479
|
+
# New, semi-arbitrary API to get data on the table
|
480
|
+
#
|
481
|
+
def describe_table(table_name, reload = false)
|
482
|
+
(!reload && table_cache[table_name]) || begin
|
483
|
+
table_cache[table_name] = Table.new(client.describe_table(table_name: table_name).data)
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
#
|
488
|
+
# Converts a hash returned by get_item, scan, etc. into a key-value hash
|
489
|
+
#
|
490
|
+
def result_item_to_hash(item)
|
491
|
+
{}.tap do |r|
|
492
|
+
item.each { |k,v| r[k.to_sym] = v }
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
# Converts a Dynamoid::Indexes::Index to an AWS API-compatible hash.
|
497
|
+
# This resulting hash is of the form:
|
498
|
+
#
|
499
|
+
# {
|
500
|
+
# index_name: String
|
501
|
+
# keys: {
|
502
|
+
# hash_key: aws_key_schema (hash)
|
503
|
+
# range_key: aws_key_schema (hash)
|
504
|
+
# }
|
505
|
+
# projection: {
|
506
|
+
# projection_type: (ALL, KEYS_ONLY, INCLUDE) String
|
507
|
+
# non_key_attributes: (optional) Array
|
508
|
+
# }
|
509
|
+
# provisioned_throughput: {
|
510
|
+
# read_capacity_units: Integer
|
511
|
+
# write_capacity_units: Integer
|
512
|
+
# }
|
513
|
+
# }
|
514
|
+
#
|
515
|
+
# @param [Dynamoid::Indexes::Index] index the index.
|
516
|
+
# @return [Hash] hash representing an AWS Index definition.
|
517
|
+
def index_to_aws_hash(index)
|
518
|
+
key_schema = aws_key_schema(index.hash_key_schema, index.range_key_schema)
|
519
|
+
|
520
|
+
hash = {
|
521
|
+
:index_name => index.name,
|
522
|
+
:key_schema => key_schema,
|
523
|
+
:projection => {
|
524
|
+
:projection_type => index.projection_type.to_s.upcase
|
525
|
+
}
|
526
|
+
}
|
527
|
+
|
528
|
+
# If the projection type is include, specify the non key attributes
|
529
|
+
if index.projection_type == "INCLUDE"
|
530
|
+
hash[:projection][:non_key_attributes] = index.projected_attributes
|
531
|
+
end
|
532
|
+
|
533
|
+
# Only global secondary indexes have a separate throughput.
|
534
|
+
if index.type == :global_secondary
|
535
|
+
hash[:provisioned_throughput] = {
|
536
|
+
:read_capacity_units => index.read_capacity,
|
537
|
+
:write_capacity_units => index.write_capacity
|
538
|
+
}
|
539
|
+
end
|
540
|
+
hash
|
541
|
+
end
|
542
|
+
|
543
|
+
# Converts hash_key_schema and range_key_schema to aws_key_schema
|
544
|
+
# @param [Hash] hash_key_schema eg: {:id => :string}
|
545
|
+
# @param [Hash] range_key_schema eg: {:created_at => :number}
|
546
|
+
# @return [Array]
|
547
|
+
def aws_key_schema(hash_key_schema, range_key_schema)
|
548
|
+
schema = [{
|
549
|
+
attribute_name: hash_key_schema.keys.first.to_s,
|
550
|
+
key_type: HASH_KEY
|
551
|
+
}]
|
552
|
+
|
553
|
+
if range_key_schema.present?
|
554
|
+
schema << {
|
555
|
+
attribute_name: range_key_schema.keys.first.to_s,
|
556
|
+
key_type: RANGE_KEY
|
557
|
+
}
|
558
|
+
end
|
559
|
+
schema
|
560
|
+
end
|
561
|
+
|
562
|
+
# Builds aws attributes definitions based off of primary hash/range and
|
563
|
+
# secondary indexes
|
564
|
+
#
|
565
|
+
# @param key_data
|
566
|
+
# @option key_data [Hash] hash_key_schema - eg: {:id => :string}
|
567
|
+
# @option key_data [Hash] range_key_schema - eg: {:created_at => :number}
|
568
|
+
# @param [Hash] secondary_indexes
|
569
|
+
# @option secondary_indexes [Array<Dynamoid::Indexes::Index>] :local_secondary_indexes
|
570
|
+
# @option secondary_indexes [Array<Dynamoid::Indexes::Index>] :global_secondary_indexes
|
571
|
+
def build_all_attribute_definitions(key_schema, secondary_indexes = {})
|
572
|
+
ls_indexes = secondary_indexes[:local_secondary_indexes]
|
573
|
+
gs_indexes = secondary_indexes[:global_secondary_indexes]
|
574
|
+
|
575
|
+
attribute_definitions = []
|
576
|
+
|
577
|
+
attribute_definitions << build_attribute_definitions(
|
578
|
+
key_schema[:hash_key_schema],
|
579
|
+
key_schema[:range_key_schema]
|
580
|
+
)
|
581
|
+
|
582
|
+
if ls_indexes.present?
|
583
|
+
ls_indexes.map do |index|
|
584
|
+
attribute_definitions << build_attribute_definitions(
|
585
|
+
index.hash_key_schema,
|
586
|
+
index.range_key_schema
|
587
|
+
)
|
588
|
+
end
|
589
|
+
end
|
590
|
+
|
591
|
+
if gs_indexes.present?
|
592
|
+
gs_indexes.map do |index|
|
593
|
+
attribute_definitions << build_attribute_definitions(
|
594
|
+
index.hash_key_schema,
|
595
|
+
index.range_key_schema
|
596
|
+
)
|
597
|
+
end
|
598
|
+
end
|
599
|
+
|
600
|
+
attribute_definitions.flatten!
|
601
|
+
# uniq these definitions because range keys might be common between
|
602
|
+
# primary and secondary indexes
|
603
|
+
attribute_definitions.uniq!
|
604
|
+
attribute_definitions
|
605
|
+
end
|
606
|
+
|
607
|
+
|
608
|
+
# Builds an attribute definitions based on hash key and range key
|
609
|
+
# @params [Hash] hash_key_schema - eg: {:id => :string}
|
610
|
+
# @params [Hash] range_key_schema - eg: {:created_at => :datetime}
|
611
|
+
# @return [Array]
|
612
|
+
def build_attribute_definitions(hash_key_schema, range_key_schema = nil)
|
613
|
+
attrs = []
|
614
|
+
|
615
|
+
attrs << attribute_definition_element(
|
616
|
+
hash_key_schema.keys.first,
|
617
|
+
hash_key_schema.values.first
|
618
|
+
)
|
619
|
+
|
620
|
+
if range_key_schema.present?
|
621
|
+
attrs << attribute_definition_element(
|
622
|
+
range_key_schema.keys.first,
|
623
|
+
range_key_schema.values.first
|
624
|
+
)
|
625
|
+
end
|
626
|
+
|
627
|
+
attrs
|
628
|
+
end
|
629
|
+
|
630
|
+
# Builds an aws attribute definition based on name and dynamoid type
|
631
|
+
# @params [Symbol] name - eg: :id
|
632
|
+
# @params [Symbol] dynamoid_type - eg: :string
|
633
|
+
# @return [Hash]
|
634
|
+
def attribute_definition_element(name, dynamoid_type)
|
635
|
+
aws_type = api_type(dynamoid_type)
|
636
|
+
|
637
|
+
{
|
638
|
+
:attribute_name => name.to_s,
|
639
|
+
:attribute_type => aws_type
|
640
|
+
}
|
641
|
+
end
|
642
|
+
|
643
|
+
#
|
644
|
+
# Represents a table. Exposes data from the "DescribeTable" API call, and also
|
645
|
+
# provides methods for coercing values to the proper types based on the table's schema data
|
646
|
+
#
|
647
|
+
class Table
|
648
|
+
attr_reader :schema
|
649
|
+
|
650
|
+
#
|
651
|
+
# @param [Hash] schema Data returns from a "DescribeTable" call
|
652
|
+
#
|
653
|
+
def initialize(schema)
|
654
|
+
@schema = schema[:table]
|
655
|
+
end
|
656
|
+
|
657
|
+
def range_key
|
658
|
+
@range_key ||= schema[:key_schema].find { |d| d[:key_type] == RANGE_KEY }.try(:attribute_name)
|
659
|
+
end
|
660
|
+
|
661
|
+
def range_type
|
662
|
+
range_type ||= schema[:attribute_definitions].find { |d|
|
663
|
+
d[:attribute_name] == range_key
|
664
|
+
}.try(:fetch,:attribute_type, nil)
|
665
|
+
end
|
666
|
+
|
667
|
+
def hash_key
|
668
|
+
@hash_key ||= schema[:key_schema].find { |d| d[:key_type] == HASH_KEY }.try(:attribute_name).to_sym
|
669
|
+
end
|
670
|
+
|
671
|
+
#
|
672
|
+
# Returns the API type (e.g. "N", "S") for the given column, if the schema defines it,
|
673
|
+
# nil otherwise
|
674
|
+
#
|
675
|
+
def col_type(col)
|
676
|
+
col = col.to_s
|
677
|
+
col_def = schema[:attribute_definitions].find { |d| d[:attribute_name] == col.to_s }
|
678
|
+
col_def && col_def[:attribute_type]
|
679
|
+
end
|
680
|
+
|
681
|
+
def item_count
|
682
|
+
schema[:item_count]
|
683
|
+
end
|
684
|
+
end
|
685
|
+
|
686
|
+
#
|
687
|
+
# Mimics behavior of the yielded object on DynamoDB's update_item API (high level).
|
688
|
+
#
|
689
|
+
class ItemUpdater
|
690
|
+
attr_reader :table, :key, :range_key
|
691
|
+
|
692
|
+
def initialize(table, key, range_key = nil)
|
693
|
+
@table = table; @key = key, @range_key = range_key
|
694
|
+
@additions = {}
|
695
|
+
@deletions = {}
|
696
|
+
@updates = {}
|
697
|
+
end
|
698
|
+
|
699
|
+
#
|
700
|
+
# Adds the given values to the values already stored in the corresponding columns.
|
701
|
+
# The column must contain a Set or a number.
|
702
|
+
#
|
703
|
+
# @param [Hash] vals keys of the hash are the columns to update, vals are the values to
|
704
|
+
# add. values must be a Set, Array, or Numeric
|
705
|
+
#
|
706
|
+
def add(values)
|
707
|
+
@additions.merge!(values)
|
708
|
+
end
|
709
|
+
|
710
|
+
#
|
711
|
+
# Removes values from the sets of the given columns
|
712
|
+
#
|
713
|
+
# @param [Hash] values keys of the hash are the columns, values are Arrays/Sets of items
|
714
|
+
# to remove
|
715
|
+
#
|
716
|
+
def delete(values)
|
717
|
+
@deletions.merge!(values)
|
718
|
+
end
|
719
|
+
|
720
|
+
#
|
721
|
+
# Replaces the values of one or more attributes
|
722
|
+
#
|
723
|
+
def set(values)
|
724
|
+
@updates.merge!(values)
|
725
|
+
end
|
726
|
+
|
727
|
+
#
|
728
|
+
# Returns an AttributeUpdates hash suitable for passing to the V2 Client API
|
729
|
+
#
|
730
|
+
def to_h
|
731
|
+
ret = {}
|
732
|
+
|
733
|
+
@additions.each do |k,v|
|
734
|
+
ret[k.to_s] = {
|
735
|
+
action: ADD,
|
736
|
+
value: v
|
737
|
+
}
|
738
|
+
end
|
739
|
+
@deletions.each do |k,v|
|
740
|
+
ret[k.to_s] = {
|
741
|
+
action: DELETE,
|
742
|
+
value: v
|
743
|
+
}
|
744
|
+
end
|
745
|
+
@updates.each do |k,v|
|
746
|
+
ret[k.to_s] = {
|
747
|
+
action: PUT,
|
748
|
+
value: v
|
749
|
+
}
|
750
|
+
end
|
751
|
+
|
752
|
+
ret
|
753
|
+
end
|
754
|
+
|
755
|
+
ADD = "ADD".freeze
|
756
|
+
DELETE = "DELETE".freeze
|
757
|
+
PUT = "PUT".freeze
|
758
|
+
end
|
759
|
+
end
|
760
|
+
end
|
761
|
+
end
|