@@ -6,6 +6,18 @@ import { cloudformation as dynamodb } from './dynamodb.generated';
6
6
const HASH_KEY_TYPE = 'HASH' ;
7
7
const RANGE_KEY_TYPE = 'RANGE' ;
8
8
9
+ export interface Attribute {
10
+ /**
11
+ * The name of an attribute.
12
+ */
13
+ name : string ;
14
+
15
+ /**
16
+ * The data type of an attribute.
17
+ */
18
+ type : AttributeType ;
19
+ }
20
+
9
21
export interface TableProps {
10
22
/**
11
23
* The read capacity for the table. Careful if you add Global Secondary Indexes, as
@@ -66,16 +78,46 @@ export interface TableProps {
66
78
writeAutoScaling ?: AutoScalingProps ;
67
79
}
68
80
69
- export interface Attribute {
81
+ export interface SecondaryIndexProps {
70
82
/**
71
- * The name of an attribute .
83
+ * The name of the secondary index .
72
84
*/
73
- name : string ;
85
+ indexName : string ;
74
86
75
87
/**
76
- * The data type of an attribute .
88
+ * The attribute of a partition key for the secondary index .
77
89
*/
78
- type : AttributeType ;
90
+ partitionKey : Attribute ;
91
+
92
+ /**
93
+ * The attribute of a sort key for the secondary index.
94
+ * @default undefined
95
+ */
96
+ sortKey ?: Attribute ;
97
+
98
+ /**
99
+ * The set of attributes that are projected into the secondary index.
100
+ * @default ALL
101
+ */
102
+ projectionType ?: ProjectionType ;
103
+
104
+ /**
105
+ * The non-key attributes that are projected into the secondary index.
106
+ * @default undefined
107
+ */
108
+ nonKeyAttributes ?: string [ ] ;
109
+
110
+ /**
111
+ * The read capacity for the secondary index.
112
+ * @default 5
113
+ */
114
+ readCapacity ?: number ;
115
+
116
+ /**
117
+ * The write capacity for the secondary index.
118
+ * @default 5
119
+ */
120
+ writeCapacity ?: number ;
79
121
}
80
122
81
123
/* tslint:disable:max-line-length */
@@ -126,22 +168,23 @@ export class Table extends Construct {
126
168
127
169
private readonly keySchema = new Array < dynamodb . TableResource . KeySchemaProperty > ( ) ;
128
170
private readonly attributeDefinitions = new Array < dynamodb . TableResource . AttributeDefinitionProperty > ( ) ;
171
+ private readonly globalSecondaryIndexes = new Array < dynamodb . TableResource . GlobalSecondaryIndexProperty > ( ) ;
172
+
173
+ private readonly nonKeyAttributes : string [ ] = [ ] ;
129
174
130
175
private readScalingPolicyResource ?: applicationautoscaling . ScalingPolicyResource ;
131
176
private writeScalingPolicyResource ?: applicationautoscaling . ScalingPolicyResource ;
132
177
133
178
constructor ( parent : Construct , name : string , props : TableProps = { } ) {
134
179
super ( parent , name ) ;
135
180
136
- const readCapacityUnits = props . readCapacity || 5 ;
137
- const writeCapacityUnits = props . writeCapacity || 5 ;
138
-
139
181
this . table = new dynamodb . TableResource ( this , 'Resource' , {
140
182
tableName : props . tableName ,
141
183
keySchema : this . keySchema ,
142
184
attributeDefinitions : this . attributeDefinitions ,
185
+ globalSecondaryIndexes : this . globalSecondaryIndexes ,
143
186
pointInTimeRecoverySpecification : props . pitrEnabled ? { pointInTimeRecoveryEnabled : props . pitrEnabled } : undefined ,
144
- provisionedThroughput : { readCapacityUnits, writeCapacityUnits } ,
187
+ provisionedThroughput : { readCapacityUnits : props . readCapacity || 5 , writeCapacityUnits : props . writeCapacity || 5 } ,
145
188
sseSpecification : props . sseEnabled ? { sseEnabled : props . sseEnabled } : undefined ,
146
189
streamSpecification : props . streamSpecification ? { streamViewType : props . streamSpecification } : undefined ,
147
190
timeToLiveSpecification : props . ttlAttributeName ? { attributeName : props . ttlAttributeName , enabled : true } : undefined
@@ -163,15 +206,54 @@ export class Table extends Construct {
163
206
}
164
207
165
208
public addPartitionKey ( attribute : Attribute ) : this {
166
- this . addKey ( attribute . name , attribute . type , HASH_KEY_TYPE ) ;
209
+ this . addKey ( attribute , HASH_KEY_TYPE ) ;
167
210
return this ;
168
211
}
169
212
170
213
public addSortKey ( attribute : Attribute ) : this {
171
- this . addKey ( attribute . name , attribute . type , RANGE_KEY_TYPE ) ;
214
+ this . addKey ( attribute , RANGE_KEY_TYPE ) ;
172
215
return this ;
173
216
}
174
217
218
+ public addGlobalSecondaryIndex ( props : SecondaryIndexProps ) {
219
+ if ( this . globalSecondaryIndexes . length === 5 ) {
220
+ // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-secondary-indexes
221
+ throw new RangeError ( 'a maximum number of global secondary index per table is 5' ) ;
222
+ }
223
+
224
+ if ( props . projectionType === ProjectionType . Include && ! props . nonKeyAttributes ) {
225
+ // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-projectionobject.html
226
+ throw new Error ( `non-key attributes should be specified when using ${ ProjectionType . Include } projection type` ) ;
227
+ }
228
+
229
+ if ( props . projectionType !== ProjectionType . Include && props . nonKeyAttributes ) {
230
+ // this combination causes validation exception, status code 400, while trying to create CFN stack
231
+ throw new Error ( `non-key attributes should not be specified when not using ${ ProjectionType . Include } projection type` ) ;
232
+ }
233
+
234
+ // build key schema for index
235
+ const gsiKeySchema = this . buildIndexKeySchema ( props . partitionKey , props . sortKey ) ;
236
+
237
+ // register attribute to check if a given configuration is valid
238
+ this . registerAttribute ( props . partitionKey ) ;
239
+ if ( props . sortKey ) {
240
+ this . registerAttribute ( props . sortKey ) ;
241
+ }
242
+ if ( props . nonKeyAttributes ) {
243
+ this . validateNonKeyAttributes ( props . nonKeyAttributes ) ;
244
+ }
245
+
246
+ this . globalSecondaryIndexes . push ( {
247
+ indexName : props . indexName ,
248
+ keySchema : gsiKeySchema ,
249
+ projection : {
250
+ projectionType : props . projectionType ? props . projectionType : ProjectionType . All ,
251
+ nonKeyAttributes : props . nonKeyAttributes ? props . nonKeyAttributes : undefined
252
+ } ,
253
+ provisionedThroughput : { readCapacityUnits : props . readCapacity || 5 , writeCapacityUnits : props . writeCapacity || 5 }
254
+ } ) ;
255
+ }
256
+
175
257
public addReadAutoScaling ( props : AutoScalingProps ) {
176
258
this . readScalingPolicyResource = this . buildAutoScaling ( this . readScalingPolicyResource , 'Read' , props ) ;
177
259
}
@@ -188,6 +270,29 @@ export class Table extends Construct {
188
270
return errors ;
189
271
}
190
272
273
+ /**
274
+ * Validate non-key attributes by checking limits within secondary index, which may vary in future.
275
+ *
276
+ * @param {string[] } nonKeyAttributes a list of non-key attribute names
277
+ */
278
+ private validateNonKeyAttributes ( nonKeyAttributes : string [ ] ) {
279
+ if ( this . nonKeyAttributes . length + nonKeyAttributes . length > 20 ) {
280
+ // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-secondary-indexes
281
+ throw new RangeError ( 'a maximum number of nonKeyAttributes across all of secondary indexes is 20' ) ;
282
+ }
283
+
284
+ // store all non-key attributes
285
+ this . nonKeyAttributes . push ( ...nonKeyAttributes ) ;
286
+
287
+ // throw error if key attribute is part of non-key attributes
288
+ this . attributeDefinitions . forEach ( keyAttribute => {
289
+ if ( typeof keyAttribute . attributeName === 'string' && this . nonKeyAttributes . includes ( keyAttribute . attributeName ) ) {
290
+ throw new Error ( `a key attribute, ${ keyAttribute . attributeName } , is part of a list of non-key attributes, ${ this . nonKeyAttributes } ` +
291
+ ', which is not allowed since all key attributes are added automatically and this configuration causes stack creation failure' ) ;
292
+ }
293
+ } ) ;
294
+ }
295
+
191
296
private validateAutoScalingProps ( props : AutoScalingProps ) {
192
297
if ( props . targetValue < 10 || props . targetValue > 90 ) {
193
298
throw new RangeError ( "scalingTargetValue for predefined metric type DynamoDBReadCapacityUtilization/"
@@ -207,6 +312,18 @@ export class Table extends Construct {
207
312
}
208
313
}
209
314
315
+ private buildIndexKeySchema ( partitionKey : Attribute , sortKey ?: Attribute ) : dynamodb . TableResource . KeySchemaProperty [ ] {
316
+ const indexKeySchema : dynamodb . TableResource . KeySchemaProperty [ ] = [
317
+ { attributeName : partitionKey . name , keyType : HASH_KEY_TYPE }
318
+ ] ;
319
+
320
+ if ( sortKey ) {
321
+ indexKeySchema . push ( { attributeName : sortKey . name , keyType : RANGE_KEY_TYPE } ) ;
322
+ }
323
+
324
+ return indexKeySchema ;
325
+ }
326
+
210
327
private buildAutoScaling ( scalingPolicyResource : applicationautoscaling . ScalingPolicyResource | undefined ,
211
328
scalingType : string ,
212
329
props : AutoScalingProps ) {
@@ -278,20 +395,27 @@ export class Table extends Construct {
278
395
return this . keySchema . find ( prop => prop . keyType === keyType ) ;
279
396
}
280
397
281
- private addKey ( name : string , type : AttributeType , keyType : string ) {
398
+ private addKey ( attribute : Attribute , keyType : string ) {
282
399
const existingProp = this . findKey ( keyType ) ;
283
400
if ( existingProp ) {
284
- throw new Error ( `Unable to set ${ name } as a ${ keyType } key, because ${ existingProp . attributeName } is a ${ keyType } key` ) ;
401
+ throw new Error ( `Unable to set ${ attribute . name } as a ${ keyType } key, because ${ existingProp . attributeName } is a ${ keyType } key` ) ;
285
402
}
286
- this . registerAttribute ( name , type ) ;
403
+ this . registerAttribute ( attribute ) ;
287
404
this . keySchema . push ( {
288
- attributeName : name ,
405
+ attributeName : attribute . name ,
289
406
keyType
290
407
} ) ;
291
408
return this ;
292
409
}
293
410
294
- private registerAttribute ( name : string , type : AttributeType ) {
411
+ /**
412
+ * Register the key attribute of table or secondary index to assemble attribute definitions of TableResourceProps.
413
+ *
414
+ * @param {Attribute } attribute the key attribute of table or secondary index
415
+ */
416
+ private registerAttribute ( attribute : Attribute ) {
417
+ const name = attribute . name ;
418
+ const type = attribute . type ;
295
419
const existingDef = this . attributeDefinitions . find ( def => def . attributeName === name ) ;
296
420
if ( existingDef && existingDef . attributeType !== type ) {
297
421
throw new Error ( `Unable to specify ${ name } as ${ type } because it was already defined as ${ existingDef . attributeType } ` ) ;
@@ -311,6 +435,12 @@ export enum AttributeType {
311
435
String = 'S' ,
312
436
}
313
437
438
+ export enum ProjectionType {
439
+ KeysOnly = 'KEYS_ONLY' ,
440
+ Include = 'INCLUDE' ,
441
+ All = 'ALL'
442
+ }
443
+
314
444
/**
315
445
* When an item in the table is modified, StreamViewType determines what information
316
446
* is written to the stream for this table. Valid values for StreamViewType are:
0 commit comments