fake_dynamo 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -30,10 +30,6 @@ module FakeDynamo
30
30
  raise UnknownOperationException, "Unknown operation: #{operation}" unless available_operations.include? operation
31
31
  end
32
32
 
33
- def available_operations
34
- api_config[:operations].keys
35
- end
36
-
37
33
  def validate_input(operation, data)
38
34
  api_input_spec(operation).each do |attribute, spec|
39
35
  validate_spec(attribute, data[attribute], spec, [])
@@ -71,7 +67,7 @@ module FakeDynamo
71
67
  when :within
72
68
  range = constrain[:within]
73
69
  unless range.include? data.size
74
- add_errors("The parameter '#{param(attribute, parents)}' value '#{data}' should be within #{range} characters")
70
+ add_errors("The parameter '#{param(attribute, parents)}' value '#{data}' should be within #{range}")
75
71
  end
76
72
  when :enum
77
73
  enum = constrain[:enum]
@@ -81,7 +77,7 @@ module FakeDynamo
81
77
  when :structure
82
78
  structure = constrain[:structure]
83
79
  structure.each do |attribute, spec|
84
- validate_spec(attribute, data[attribute], spec, new_parents)
80
+ validate_spec(attribute, data[attribute], spec, parents + ["member"])
85
81
  end
86
82
  when :map
87
83
  map = constrain[:map]
@@ -92,8 +88,8 @@ module FakeDynamo
92
88
  end
93
89
  when :list
94
90
  raise "#{param(attribute, parents)} must be a Array" unless data.kind_of? Array
95
- data.each do |element|
96
- validate_spec(element, element, constrain[:list], new_parents)
91
+ data.each_with_index do |element, i|
92
+ validate_spec(element, element, constrain[:list], new_parents + [(i+1).to_s])
97
93
  end
98
94
  else
99
95
  raise "Unhandled constraint #{constrain}"
@@ -111,7 +107,11 @@ module FakeDynamo
111
107
  end
112
108
 
113
109
  def api_input_spec(operation)
114
- api_config[:operations][operation][:input]
110
+ api_config[:operations].find { |spec| spec[:name] == operation }[:inputs]
111
+ end
112
+
113
+ def available_operations
114
+ @available_operations ||= api_config[:operations].map { |spec| spec[:name] }
115
115
  end
116
116
 
117
117
  def api_config
@@ -119,7 +119,7 @@ module FakeDynamo
119
119
  end
120
120
 
121
121
  def api_config_path
122
- File.join File.expand_path(File.dirname(__FILE__)), 'api.yml'
122
+ File.join File.expand_path(File.dirname(__FILE__)), 'api_2012-08-10.yml'
123
123
  end
124
124
 
125
125
  def validate_type(value, attribute)
@@ -144,21 +144,73 @@ module FakeDynamo
144
144
  end
145
145
 
146
146
  def validate_key_data(data, key_schema)
147
- validate_type(data['HashKeyElement'], key_schema.hash_key)
147
+ hash_key = data[key_schema.hash_key.name] or key_schema_mismatch
148
+ validate_type(hash_key, key_schema.hash_key)
148
149
 
149
150
  if key_schema.range_key
150
- range_key = data['RangeKeyElement'] or raise ValidationException, "Missing the key RangeKeyElement in the Key"
151
+ range_key = data[key_schema.range_key.name] or key_schema_mismatch
151
152
  validate_type(range_key, key_schema.range_key)
152
- elsif data['RangeKeyElement']
153
- raise ValidationException, "RangeKeyElement is not present in the schema"
153
+ key_schema_mismatch if data.size != 2
154
+ else
155
+ key_schema_mismatch if data.size != 1
154
156
  end
155
157
  end
156
158
 
159
+ def key_schema_mismatch
160
+ raise ValidationException, "The provided key element does not match the schema"
161
+ end
162
+
157
163
  def validate_request_size(data)
158
164
  if data.to_s.bytesize > 1 * 1024 * 1024
159
165
  raise ValidationException, "Request size can't exceed 1 mb"
160
166
  end
161
167
  end
162
168
 
169
+ def validate_range_key(key_schema)
170
+ unless key_schema.range_key
171
+ raise ValidationException, 'Table KeySchema does not have a range key'
172
+ end
173
+ end
174
+
175
+ def validate_hash_key(index, table)
176
+ if index.hash_key != table.hash_key
177
+ raise ValidationException, "Index KeySchema does not have the same leading hash key as table KeySchema for index"
178
+ end
179
+ end
180
+
181
+ def validate_projection(projection)
182
+ if projection.type == 'INCLUDE'
183
+ unless projection.non_key_attributes
184
+ raise ValidationException, "ProjectionType is #{projection.type}, but NonKeyAttributes is not specified"
185
+ end
186
+ else
187
+ if projection.non_key_attributes
188
+ raise ValidationException, "ProjectionType is #{projection.type}, but NonKeyAttributes is specified"
189
+ end
190
+ end
191
+ end
192
+
193
+ def validate_index_names(indexes)
194
+ names = indexes.map(&:name)
195
+ if names.uniq.size != names.size
196
+ raise ValidationException, "Duplicate index name: #{names.find { |n| names.count(n) > 1 }}"
197
+ end
198
+ end
199
+
200
+ def validate_hash_condition(condition)
201
+ unless condition
202
+ raise ValidationException, "Query condition missed key schema element #{key_schema.hash_key.name}"
203
+ end
204
+
205
+ if condition['ComparisonOperator'] != 'EQ'
206
+ raise ValidationException, "Query key condition not supported"
207
+ end
208
+ end
209
+
210
+ def validate_range_condition(condition, schema)
211
+ unless condition.has_key?(schema.range_key.name)
212
+ raise ValidationException, "Query condition missed key schema element #{schema.range_key.name}"
213
+ end
214
+ end
163
215
  end
164
216
  end
@@ -1,3 +1,3 @@
1
1
  module FakeDynamo
2
- VERSION = "0.1.4"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/fake_dynamo.rb CHANGED
@@ -6,6 +6,8 @@ require 'active_support/core_ext/class/attribute'
6
6
  require 'fake_dynamo/exceptions'
7
7
  require 'fake_dynamo/validation'
8
8
  require 'fake_dynamo/filter'
9
+ require 'fake_dynamo/local_secondary_index'
10
+ require 'fake_dynamo/projection'
9
11
  require 'fake_dynamo/attribute'
10
12
  require 'fake_dynamo/key_schema'
11
13
  require 'fake_dynamo/item'
@@ -5,24 +5,107 @@ module FakeDynamo
5
5
  let(:data) do
6
6
  {
7
7
  "TableName" => "Table1",
8
+ "AttributeDefinitions" =>
9
+ [{"AttributeName" => "AttributeName1","AttributeType" => "S"},
10
+ {"AttributeName" => "AttributeName2","AttributeType" => "N"}],
8
11
  "KeySchema" =>
9
- {"HashKeyElement" => {"AttributeName" => "AttributeName1","AttributeType" => "S"},
10
- "RangeKeyElement" => {"AttributeName" => "AttributeName2","AttributeType" => "N"}},
12
+ [{"AttributeName" => "AttributeName1","KeyType" => "HASH"},
13
+ {"AttributeName" => "AttributeName2","KeyType" => "RANGE"}],
11
14
  "ProvisionedThroughput" => {"ReadCapacityUnits" => 5,"WriteCapacityUnits" => 10}
12
15
  }
13
16
  end
14
17
 
15
18
  let(:user_table) do
16
19
  {"TableName" => "User",
20
+ "AttributeDefinitions" =>
21
+ [{"AttributeName" => "id","AttributeType" => "S"}],
17
22
  "KeySchema" =>
18
- {"HashKeyElement" => {"AttributeName" => "id","AttributeType" => "S"}},
23
+ [{"AttributeName" => "id","KeyType" => "HASH"}],
19
24
  "ProvisionedThroughput" => {"ReadCapacityUnits" => 5,"WriteCapacityUnits" => 10}
20
25
  }
21
26
  end
22
27
 
23
- it 'should not allow to create duplicate tables' do
24
- subject.create_table(data)
25
- expect { subject.create_table(data) }.to raise_error(ResourceInUseException, /duplicate/i)
28
+ let(:user_table_a) do
29
+ {"TableName" => "User",
30
+ "AttributeDefinitions" =>
31
+ [{"AttributeName" => "id","AttributeType" => "S"},
32
+ {"AttributeName" => "age","AttributeType" => "S"},
33
+ {"AttributeName" => "name","AttributeType" => "S"}],
34
+ "KeySchema" =>
35
+ [{"AttributeName" => "id","KeyType" => "HASH"},
36
+ {"AttributeName" => "age", "KeyType" => "RANGE"}],
37
+ "LocalSecondaryIndexes" =>
38
+ [{"IndexName" => "age",
39
+ "KeySchema" =>
40
+ [{"AttributeName" => "id", "KeyType" => "HASH"},
41
+ {"AttributeName" => "name", "KeyType" => "RANGE"}],
42
+ "Projection" => {
43
+ "ProjectionType" => "INCLUDE",
44
+ "NonKeyAttributes" => ["name", "gender"]
45
+ }
46
+ }],
47
+ "ProvisionedThroughput" => {"ReadCapacityUnits" => 5,"WriteCapacityUnits" => 10}
48
+ }
49
+ end
50
+
51
+ context 'CreateTable' do
52
+ it 'should not allow to create duplicate tables' do
53
+ subject.create_table(data)
54
+ expect { subject.create_table(data) }.to raise_error(ResourceInUseException, /duplicate/i)
55
+ end
56
+
57
+ it 'should allow to create table with secondary indexes' do
58
+ subject.create_table(user_table_a)
59
+ end
60
+
61
+ it 'should fail on extra attribute' do
62
+ user_table_a['AttributeDefinitions'] << {"AttributeName" => "gender","AttributeType" => "S"}
63
+ expect { subject.create_table(user_table_a) }.to raise_error(ValidationException, /some attributedefinitions.*not.*used/i)
64
+ end
65
+
66
+ it 'should fail on missing attribute' do
67
+ user_table_a['AttributeDefinitions'].delete_at(1)
68
+ expect { subject.create_table(user_table_a) }.to raise_error(ValidationException, /some.*attributes.*not.*defined/i)
69
+ end
70
+
71
+ context 'LocalSecondaryIndex' do
72
+ let(:lsi) { user_table_a['LocalSecondaryIndexes'][0] }
73
+
74
+ it 'should fail on invalid KeyType' do
75
+ lsi["KeySchema"][0]['KeyType'] = 'invalid'
76
+ expect { subject.process('CreateTable', user_table_a) }.to raise_error(ValidationException, /invalid.*enum/)
77
+ end
78
+
79
+ it 'should fail if range key is missing' do
80
+ lsi['KeySchema'].delete_at(1)
81
+ expect { subject.create_table(user_table_a) }.to raise_error(ValidationException, /not.*range.*key/i)
82
+ end
83
+
84
+ it 'should fail on duplicate index names' do
85
+ duplicate = lsi.clone()
86
+ user_table_a['LocalSecondaryIndexes'] << duplicate
87
+ expect { subject.create_table(user_table_a) }.to raise_error(ValidationException, /duplicate index/i)
88
+ end
89
+
90
+ it 'should fail on different hash key' do
91
+ lsi['KeySchema'][0]['AttributeName'] = 'age'
92
+ expect { subject.create_table(user_table_a) }.to raise_error(ValidationException, /not have.*same.*hash key/i)
93
+ end
94
+
95
+ context 'projection' do
96
+ let(:projection) { user_table_a['LocalSecondaryIndexes'][0]['Projection'] }
97
+
98
+ it 'should fail if non key attributes are specified unnecessarily' do
99
+ projection['ProjectionType'] = 'KEYS_ONLY'
100
+ expect { subject.create_table(user_table_a) }.to raise_error(ValidationException, /NonKeyAttributes.*specified/i)
101
+ end
102
+
103
+ it 'should fail if non key attributes are not specified' do
104
+ projection.delete('NonKeyAttributes')
105
+ expect { subject.create_table(user_table_a) }.to raise_error(ValidationException, /NonKeyAttributes.*not.*specified/i)
106
+ end
107
+ end
108
+ end
26
109
  end
27
110
 
28
111
  it 'should fail on unknown operation' do
@@ -33,7 +116,7 @@ module FakeDynamo
33
116
  it 'should describe table' do
34
117
  table = subject.create_table(data)
35
118
  description = subject.describe_table({'TableName' => 'Table1'})
36
- description.should include({
119
+ description['Table'].should include({
37
120
  "ItemCount"=>0,
38
121
  "TableSizeBytes"=>0})
39
122
  end
@@ -134,22 +217,22 @@ module FakeDynamo
134
217
  subject.process('GetItem', {
135
218
  'TableName' => 'Table1',
136
219
  'Key' => {
137
- 'HashKeyElement' => { 'S' => 'test' },
138
- 'RangeKeyElement' => { 'N' => '11' }
220
+ 'AttributeName1' => { 'S' => 'test' },
221
+ 'AttributeName2' => { 'N' => '11' }
139
222
  },
140
223
  'AttributesToGet' => ['AttributeName3']
141
224
  })
142
225
  subject.process('DeleteItem', {
143
226
  'TableName' => 'Table1',
144
227
  'Key' => {
145
- 'HashKeyElement' => { 'S' => 'test' },
146
- 'RangeKeyElement' => { 'N' => '11' }
228
+ 'AttributeName1' => { 'S' => 'test' },
229
+ 'AttributeName2' => { 'N' => '11' }
147
230
  }})
148
231
  subject.process('UpdateItem', {
149
232
  'TableName' => 'Table1',
150
233
  'Key' => {
151
- 'HashKeyElement' => { 'S' => 'test' },
152
- 'RangeKeyElement' => { 'N' => '11' }
234
+ 'AttributeName1' => { 'S' => 'test' },
235
+ 'AttributeName2' => { 'N' => '11' }
153
236
  },
154
237
  'AttributeUpdates' =>
155
238
  {'AttributeName3' =>
@@ -163,10 +246,15 @@ module FakeDynamo
163
246
  'TableName' => 'Table1',
164
247
  'Limit' => 5,
165
248
  'Count' => true,
166
- 'HashKeyValue' => {'S' => 'att1'},
167
- 'RangeKeyCondition' => {
168
- 'AttributeValueList' => [{'N' => '1'}],
169
- 'ComparisonOperator' => 'GT'
249
+ 'KeyConditions' => {
250
+ 'AttributeName1' => {
251
+ 'AttributeValueList' => [{'S' => 'att1'}],
252
+ 'ComparisonOperator' => 'EQ'
253
+ },
254
+ 'AttributeName2' => {
255
+ 'AttributeValueList' => [{'N' => '1'}],
256
+ 'ComparisonOperator' => 'GT'
257
+ }
170
258
  },
171
259
  'ScanIndexForward' => true
172
260
  })
@@ -205,25 +293,22 @@ module FakeDynamo
205
293
  response = subject.process('BatchGetItem', { 'RequestItems' =>
206
294
  {
207
295
  'User' => {
208
- 'Keys' => [{ 'HashKeyElement' => { 'S' => '1' }},
209
- { 'HashKeyElement' => { 'S' => '2' }}]
296
+ 'Keys' => [{ 'id' => { 'S' => '1' }},
297
+ { 'id' => { 'S' => '2' }}]
210
298
  },
211
299
  'Table1' => {
212
- 'Keys' => [{'HashKeyElement' => { 'S' => 'test' },
213
- 'RangeKeyElement' => { 'N' => '11' }}],
300
+ 'Keys' => [{'AttributeName1' => { 'S' => 'test' },
301
+ 'AttributeName2' => { 'N' => '11' }}],
214
302
  'AttributesToGet' => ['AttributeName1', 'AttributeName2']
215
303
  }
216
304
  }})
217
305
 
218
306
  response.should eq({"Responses"=>
219
307
  {"User"=>
220
- {"ConsumedCapacityUnits"=>1,
221
- "Items"=>[{"id"=>{"S"=>"1"}}, {"id"=>{"S"=>"2"}}]},
308
+ [{"id"=>{"S"=>"1"}}, {"id"=>{"S"=>"2"}}],
222
309
  "Table1"=>
223
- {"ConsumedCapacityUnits"=>1,
224
- "Items"=>
225
- [{"AttributeName1"=>{"S"=>"test"},
226
- "AttributeName2"=>{"N"=>"11"}}]}},
310
+ [{"AttributeName1"=>{"S"=>"test"},
311
+ "AttributeName2"=>{"N"=>"11"}}]},
227
312
  "UnprocessedKeys"=>{}})
228
313
  end
229
314
 
@@ -231,15 +316,15 @@ module FakeDynamo
231
316
  response = subject.process('BatchGetItem', { 'RequestItems' =>
232
317
  {
233
318
  'User' => {
234
- 'Keys' => [{ 'HashKeyElement' => { 'S' => '1' }},
235
- { 'HashKeyElement' => { 'S' => 'asd' }}]
319
+ 'Keys' => [{ 'id' => { 'S' => '1' }},
320
+ { 'id' => { 'S' => 'asd' }}]
236
321
  }
237
- }})
322
+ },
323
+ 'ReturnConsumedCapacity' => 'TOTAL'})
238
324
  response.should eq({"Responses"=>
239
- {"User"=>
240
- {"ConsumedCapacityUnits"=>1,
241
- "Items"=>[{"id"=>{"S"=>"1"}}]}},
242
- "UnprocessedKeys"=>{}})
325
+ {"User"=> [{"id"=>{"S"=>"1"}}]},
326
+ "UnprocessedKeys"=>{},
327
+ "ConsumedCapacity" => ['CapacityUnits' => 1, 'TableName' => 'User']})
243
328
  end
244
329
 
245
330
  it 'should fail if table not found' do
@@ -247,8 +332,8 @@ module FakeDynamo
247
332
  subject.process('BatchGetItem', { 'RequestItems' =>
248
333
  {
249
334
  'xxx' => {
250
- 'Keys' => [{ 'HashKeyElement' => { 'S' => '1' }},
251
- { 'HashKeyElement' => { 'S' => 'asd' }}]}
335
+ 'Keys' => [{ 'AttributeName1' => { 'S' => '1' }},
336
+ { 'AttributeName1' => { 'S' => 'asd' }}]}
252
337
  }})
253
338
  }.to raise_error(FakeDynamo::ResourceNotFoundException)
254
339
  end
@@ -261,6 +346,8 @@ module FakeDynamo
261
346
  db
262
347
  end
263
348
 
349
+ let(:consumed_capacity) { {'ConsumedCapacity' => { 'CapacityUnits' => 1, 'TableName' => 'User' }} }
350
+
264
351
  it 'should validate payload' do
265
352
  expect {
266
353
  subject.process('BatchWriteItem', {})
@@ -271,7 +358,7 @@ module FakeDynamo
271
358
  expect {
272
359
  subject.process('BatchWriteItem', {
273
360
  'RequestItems' => {
274
- 'xxx' => ['DeleteRequest' => { 'Key' => { 'HashKeyElement' => { 'S' => 'ananth' }}}]
361
+ 'xxx' => ['DeleteRequest' => { 'Key' => { 'AttributeName1' => { 'S' => 'ananth' }}}]
275
362
  }
276
363
  })
277
364
  }.to raise_error(FakeDynamo::ResourceNotFoundException, /table.*not.*found/i)
@@ -281,8 +368,8 @@ module FakeDynamo
281
368
  expect {
282
369
  subject.process('BatchWriteItem', {
283
370
  'RequestItems' => {
284
- 'User' => [{ 'DeleteRequest' => { 'Key' => { 'HashKeyElement' => { 'S' => 'ananth' }}}},
285
- { 'DeleteRequest' => { 'Key' => { 'HashKeyElement' => { 'S' => 'ananth' }}}}]
371
+ 'User' => [{ 'DeleteRequest' => { 'Key' => { 'id' => { 'S' => 'ananth' }}}},
372
+ { 'DeleteRequest' => { 'Key' => { 'id' => { 'S' => 'ananth' }}}}]
286
373
  }
287
374
  })
288
375
  }.to raise_error(FakeDynamo::ValidationException, /duplicate/i)
@@ -290,7 +377,7 @@ module FakeDynamo
290
377
  expect {
291
378
  subject.process('BatchWriteItem', {
292
379
  'RequestItems' => {
293
- 'User' => [{ 'DeleteRequest' => { 'Key' => { 'HashKeyElement' => { 'S' => 'ananth' }}}},
380
+ 'User' => [{ 'DeleteRequest' => { 'Key' => { 'id' => { 'S' => 'ananth' }}}},
294
381
  {'PutRequest' => {'Item' => { 'id' => { 'S' => 'ananth'}}}}]
295
382
  }
296
383
  })
@@ -310,31 +397,38 @@ module FakeDynamo
310
397
  response = subject.process('BatchWriteItem', {
311
398
  'RequestItems' => {
312
399
  'User' => [{'PutRequest' => {'Item' => { 'id' => { 'S' => 'ananth'}}}}]
313
- }
400
+ },
401
+ 'ReturnConsumedCapacity' => 'TOTAL'
314
402
  })
315
-
316
- response['Responses'].should eq('User' => { 'ConsumedCapacityUnits' => 1 })
403
+ response['ItemCollectionMetrics'].should be_nil
404
+ response.should eq('ConsumedCapacity' => [consumed_capacity['ConsumedCapacity']],
405
+ 'UnprocessedItems' => {})
317
406
 
318
407
  response = subject.get_item({'TableName' => 'User',
319
- 'Key' => {'HashKeyElement' => { 'S' => 'ananth'}}})
408
+ 'Key' => {'id' => { 'S' => 'ananth'}}})
320
409
 
321
410
  response['Item']['id'].should eq('S' => 'ananth')
322
411
 
323
- subject.process('BatchWriteItem', {
324
- 'RequestItems' => {
325
- 'User' => [{ 'DeleteRequest' => { 'Key' => { 'HashKeyElement' => { 'S' => 'ananth' }}}}]
326
- }
327
- })
412
+ response = subject.process('BatchWriteItem', {
413
+ 'RequestItems' => {
414
+ 'User' => [{ 'DeleteRequest' => { 'Key' => { 'id' => { 'S' => 'ananth' }}}}]
415
+ },
416
+ 'ReturnItemCollectionMetrics' => 'SIZE'
417
+ })
418
+
419
+ response['ItemCollectionMetrics'].should_not be_nil
328
420
 
329
421
  response = subject.get_item({'TableName' => 'User',
330
- 'Key' => {'HashKeyElement' => { 'S' => 'ananth'}}})
422
+ 'Key' => {'id' => { 'S' => 'ananth'}},
423
+ 'ReturnConsumedCapacity' => 'TOTAL'})
424
+
425
+ response.should eq(consumed_capacity)
331
426
 
332
- response.should eq({"ConsumedCapacityUnits"=>1})
333
427
  end
334
428
 
335
429
  it 'fails it the requested operation is more than 25' do
336
430
  expect {
337
- requests = (1..26).map { |i| { 'DeleteRequest' => { 'Key' => { 'HashKeyElement' => { 'S' => "ananth#{i}" }}}} }
431
+ requests = (1..26).map { |i| { 'DeleteRequest' => { 'Key' => { 'id' => { 'S' => "ananth#{i}" }}}} }
338
432
 
339
433
  subject.process('BatchWriteItem', {
340
434
  'RequestItems' => {
@@ -342,7 +436,7 @@ module FakeDynamo
342
436
  }
343
437
  })
344
438
 
345
- }.to raise_error(FakeDynamo::ValidationException, /too many items/i)
439
+ }.to raise_error(FakeDynamo::ValidationException, /within.*25/i)
346
440
  end
347
441
 
348
442
  it 'should fail on request size greater than 1 mb' do
@@ -7,9 +7,12 @@ module FakeDynamo
7
7
  let(:data) do
8
8
  {
9
9
  "TableName" => "Table1",
10
+ "AttributeDefinitions" =>
11
+ [{"AttributeName" => "AttributeName1","AttributeType" => "S"},
12
+ {"AttributeName" => "AttributeName2","AttributeType" => "N"}],
10
13
  "KeySchema" =>
11
- {"HashKeyElement" => {"AttributeName" => "AttributeName1","AttributeType" => "S"},
12
- "RangeKeyElement" => {"AttributeName" => "AttributeName2","AttributeType" => "N"}},
14
+ [{"AttributeName" => "AttributeName1","KeyType" => "HASH"},
15
+ {"AttributeName" => "AttributeName2","KeyType" => "RANGE"}],
13
16
  "ProvisionedThroughput" => {"ReadCapacityUnits" => 5,"WriteCapacityUnits" => 10}
14
17
  }
15
18
  end
@@ -6,8 +6,10 @@ module FakeDynamo
6
6
 
7
7
  let(:table) do
8
8
  {"TableName" => "User",
9
+ "AttributeDefinitions" =>
10
+ [{"AttributeName" => "id","AttributeType" => "S"}],
9
11
  "KeySchema" =>
10
- {"HashKeyElement" => {"AttributeName" => "id","AttributeType" => "S"}},
12
+ [{"AttributeName" => "id","KeyType" => "HASH"}],
11
13
  "ProvisionedThroughput" => {"ReadCapacityUnits" => 5,"WriteCapacityUnits" => 10}
12
14
  }
13
15
  end