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