@@ -6,6 +6,18 @@ import { cloudformation as dynamodb } from './dynamodb.generated';
66const HASH_KEY_TYPE = 'HASH' ;
77const RANGE_KEY_TYPE = 'RANGE' ;
88
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+
921export interface TableProps {
1022 /**
1123 * The read capacity for the table. Careful if you add Global Secondary Indexes, as
@@ -66,16 +78,46 @@ export interface TableProps {
6678 writeAutoScaling ?: AutoScalingProps ;
6779}
6880
69- export interface Attribute {
81+ export interface SecondaryIndexProps {
7082 /**
71- * The name of an attribute .
83+ * The name of the secondary index .
7284 */
73- name : string ;
85+ indexName : string ;
7486
7587 /**
76- * The data type of an attribute .
88+ * The attribute of a partition key for the secondary index .
7789 */
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 ;
79121}
80122
81123/* tslint:disable:max-line-length */
@@ -126,22 +168,23 @@ export class Table extends Construct {
126168
127169 private readonly keySchema = new Array < dynamodb . TableResource . KeySchemaProperty > ( ) ;
128170 private readonly attributeDefinitions = new Array < dynamodb . TableResource . AttributeDefinitionProperty > ( ) ;
171+ private readonly globalSecondaryIndexes = new Array < dynamodb . TableResource . GlobalSecondaryIndexProperty > ( ) ;
172+
173+ private readonly nonKeyAttributes : string [ ] = [ ] ;
129174
130175 private readScalingPolicyResource ?: applicationautoscaling . ScalingPolicyResource ;
131176 private writeScalingPolicyResource ?: applicationautoscaling . ScalingPolicyResource ;
132177
133178 constructor ( parent : Construct , name : string , props : TableProps = { } ) {
134179 super ( parent , name ) ;
135180
136- const readCapacityUnits = props . readCapacity || 5 ;
137- const writeCapacityUnits = props . writeCapacity || 5 ;
138-
139181 this . table = new dynamodb . TableResource ( this , 'Resource' , {
140182 tableName : props . tableName ,
141183 keySchema : this . keySchema ,
142184 attributeDefinitions : this . attributeDefinitions ,
185+ globalSecondaryIndexes : this . globalSecondaryIndexes ,
143186 pointInTimeRecoverySpecification : props . pitrEnabled ? { pointInTimeRecoveryEnabled : props . pitrEnabled } : undefined ,
144- provisionedThroughput : { readCapacityUnits, writeCapacityUnits } ,
187+ provisionedThroughput : { readCapacityUnits : props . readCapacity || 5 , writeCapacityUnits : props . writeCapacity || 5 } ,
145188 sseSpecification : props . sseEnabled ? { sseEnabled : props . sseEnabled } : undefined ,
146189 streamSpecification : props . streamSpecification ? { streamViewType : props . streamSpecification } : undefined ,
147190 timeToLiveSpecification : props . ttlAttributeName ? { attributeName : props . ttlAttributeName , enabled : true } : undefined
@@ -163,15 +206,54 @@ export class Table extends Construct {
163206 }
164207
165208 public addPartitionKey ( attribute : Attribute ) : this {
166- this . addKey ( attribute . name , attribute . type , HASH_KEY_TYPE ) ;
209+ this . addKey ( attribute , HASH_KEY_TYPE ) ;
167210 return this ;
168211 }
169212
170213 public addSortKey ( attribute : Attribute ) : this {
171- this . addKey ( attribute . name , attribute . type , RANGE_KEY_TYPE ) ;
214+ this . addKey ( attribute , RANGE_KEY_TYPE ) ;
172215 return this ;
173216 }
174217
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+
175257 public addReadAutoScaling ( props : AutoScalingProps ) {
176258 this . readScalingPolicyResource = this . buildAutoScaling ( this . readScalingPolicyResource , 'Read' , props ) ;
177259 }
@@ -188,6 +270,29 @@ export class Table extends Construct {
188270 return errors ;
189271 }
190272
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+
191296 private validateAutoScalingProps ( props : AutoScalingProps ) {
192297 if ( props . targetValue < 10 || props . targetValue > 90 ) {
193298 throw new RangeError ( "scalingTargetValue for predefined metric type DynamoDBReadCapacityUtilization/"
@@ -207,6 +312,18 @@ export class Table extends Construct {
207312 }
208313 }
209314
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+
210327 private buildAutoScaling ( scalingPolicyResource : applicationautoscaling . ScalingPolicyResource | undefined ,
211328 scalingType : string ,
212329 props : AutoScalingProps ) {
@@ -278,20 +395,27 @@ export class Table extends Construct {
278395 return this . keySchema . find ( prop => prop . keyType === keyType ) ;
279396 }
280397
281- private addKey ( name : string , type : AttributeType , keyType : string ) {
398+ private addKey ( attribute : Attribute , keyType : string ) {
282399 const existingProp = this . findKey ( keyType ) ;
283400 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` ) ;
285402 }
286- this . registerAttribute ( name , type ) ;
403+ this . registerAttribute ( attribute ) ;
287404 this . keySchema . push ( {
288- attributeName : name ,
405+ attributeName : attribute . name ,
289406 keyType
290407 } ) ;
291408 return this ;
292409 }
293410
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 ;
295419 const existingDef = this . attributeDefinitions . find ( def => def . attributeName === name ) ;
296420 if ( existingDef && existingDef . attributeType !== type ) {
297421 throw new Error ( `Unable to specify ${ name } as ${ type } because it was already defined as ${ existingDef . attributeType } ` ) ;
@@ -311,6 +435,12 @@ export enum AttributeType {
311435 String = 'S' ,
312436}
313437
438+ export enum ProjectionType {
439+ KeysOnly = 'KEYS_ONLY' ,
440+ Include = 'INCLUDE' ,
441+ All = 'ALL'
442+ }
443+
314444/**
315445 * When an item in the table is modified, StreamViewType determines what information
316446 * is written to the stream for this table. Valid values for StreamViewType are:
0 commit comments