dynamoid-edge 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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