Skip to content

Commit 4980c97

Browse files
jungseokleerix0rrr
authored andcommitted
feat(aws-dynamodb): support Global Secondary Indexes (#760)
Add supports for adding GSIs to DynamoDB tables.
1 parent a5089e9 commit 4980c97

File tree

4 files changed

+997
-189
lines changed

4 files changed

+997
-189
lines changed

Diff for: packages/@aws-cdk/aws-dynamodb/lib/table.ts

+146-16
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ import { cloudformation as dynamodb } from './dynamodb.generated';
66
const HASH_KEY_TYPE = 'HASH';
77
const 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+
921
export 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

Comments
 (0)