Skip to content

Commit a67b2d9

Browse files
jungseokleerix0rrr
authored andcommitted
feat(aws-dynamodB): support Local Secondary Indexes (#825)
Adds support for specifying Local Secondary Indexes on DynamoDB tables.
1 parent ed1e1e4 commit a67b2d9

File tree

4 files changed

+656
-94
lines changed

4 files changed

+656
-94
lines changed

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

+131-40
Original file line numberDiff line numberDiff line change
@@ -85,41 +85,50 @@ export interface SecondaryIndexProps {
8585
indexName: string;
8686

8787
/**
88-
* The attribute of a partition key for the secondary index.
88+
* The set of attributes that are projected into the secondary index.
89+
* @default ALL
8990
*/
90-
partitionKey: Attribute;
91+
projectionType?: ProjectionType;
9192

9293
/**
93-
* The attribute of a sort key for the secondary index.
94+
* The non-key attributes that are projected into the secondary index.
9495
* @default undefined
9596
*/
96-
sortKey?: Attribute;
97+
nonKeyAttributes?: string[];
98+
}
9799

100+
export interface GlobalSecondaryIndexProps extends SecondaryIndexProps {
98101
/**
99-
* The set of attributes that are projected into the secondary index.
100-
* @default ALL
102+
* The attribute of a partition key for the global secondary index.
101103
*/
102-
projectionType?: ProjectionType;
104+
partitionKey: Attribute;
103105

104106
/**
105-
* The non-key attributes that are projected into the secondary index.
107+
* The attribute of a sort key for the global secondary index.
106108
* @default undefined
107109
*/
108-
nonKeyAttributes?: string[];
110+
sortKey?: Attribute;
109111

110112
/**
111-
* The read capacity for the secondary index.
113+
* The read capacity for the global secondary index.
112114
* @default 5
113115
*/
114116
readCapacity?: number;
115117

116118
/**
117-
* The write capacity for the secondary index.
119+
* The write capacity for the global secondary index.
118120
* @default 5
119121
*/
120122
writeCapacity?: number;
121123
}
122124

125+
export interface LocalSecondaryIndexProps extends SecondaryIndexProps {
126+
/**
127+
* The attribute of a sort key for the local secondary index.
128+
*/
129+
sortKey: Attribute;
130+
}
131+
123132
/* tslint:disable:max-line-length */
124133
export interface AutoScalingProps {
125134
/**
@@ -169,9 +178,14 @@ export class Table extends Construct {
169178
private readonly keySchema = new Array<dynamodb.TableResource.KeySchemaProperty>();
170179
private readonly attributeDefinitions = new Array<dynamodb.TableResource.AttributeDefinitionProperty>();
171180
private readonly globalSecondaryIndexes = new Array<dynamodb.TableResource.GlobalSecondaryIndexProperty>();
181+
private readonly localSecondaryIndexes = new Array<dynamodb.TableResource.LocalSecondaryIndexProperty>();
172182

183+
private readonly secondaryIndexNames: string[] = [];
173184
private readonly nonKeyAttributes: string[] = [];
174185

186+
private tablePartitionKey: Attribute | undefined = undefined;
187+
private tableSortKey: Attribute | undefined = undefined;
188+
175189
private readScalingPolicyResource?: applicationautoscaling.ScalingPolicyResource;
176190
private writeScalingPolicyResource?: applicationautoscaling.ScalingPolicyResource;
177191

@@ -183,6 +197,7 @@ export class Table extends Construct {
183197
keySchema: this.keySchema,
184198
attributeDefinitions: this.attributeDefinitions,
185199
globalSecondaryIndexes: this.globalSecondaryIndexes,
200+
localSecondaryIndexes: this.localSecondaryIndexes,
186201
pointInTimeRecoverySpecification: props.pitrEnabled ? { pointInTimeRecoveryEnabled: props.pitrEnabled } : undefined,
187202
provisionedThroughput: { readCapacityUnits: props.readCapacity || 5, writeCapacityUnits: props.writeCapacity || 5 },
188203
sseSpecification: props.sseEnabled ? { sseEnabled: props.sseEnabled } : undefined,
@@ -205,55 +220,85 @@ export class Table extends Construct {
205220
}
206221
}
207222

223+
/**
224+
* Add a partition key of table.
225+
*
226+
* @param attribute the partition key attribute of table
227+
* @returns a reference to this object so that method calls can be chained together
228+
*/
208229
public addPartitionKey(attribute: Attribute): this {
209230
this.addKey(attribute, HASH_KEY_TYPE);
231+
this.tablePartitionKey = attribute;
210232
return this;
211233
}
212234

235+
/**
236+
* Add a sort key of table.
237+
*
238+
* @param attribute the sort key of table
239+
* @returns a reference to this object so that method calls can be chained together
240+
*/
213241
public addSortKey(attribute: Attribute): this {
214242
this.addKey(attribute, RANGE_KEY_TYPE);
243+
this.tableSortKey = attribute;
215244
return this;
216245
}
217246

218-
public addGlobalSecondaryIndex(props: SecondaryIndexProps) {
247+
/**
248+
* Add a global secondary index of table.
249+
*
250+
* @param props the property of global secondary index
251+
*/
252+
public addGlobalSecondaryIndex(props: GlobalSecondaryIndexProps) {
219253
if (this.globalSecondaryIndexes.length === 5) {
220254
// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-secondary-indexes
221255
throw new RangeError('a maximum number of global secondary index per table is 5');
222256
}
223257

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-
}
258+
this.validateIndexName(props.indexName);
233259

234-
// build key schema for index
260+
// build key schema and projection for index
235261
const gsiKeySchema = this.buildIndexKeySchema(props.partitionKey, props.sortKey);
262+
const gsiProjection = this.buildIndexProjection(props);
236263

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-
264+
this.secondaryIndexNames.push(props.indexName);
246265
this.globalSecondaryIndexes.push({
247266
indexName: props.indexName,
248267
keySchema: gsiKeySchema,
249-
projection: {
250-
projectionType: props.projectionType ? props.projectionType : ProjectionType.All,
251-
nonKeyAttributes: props.nonKeyAttributes ? props.nonKeyAttributes : undefined
252-
},
268+
projection: gsiProjection,
253269
provisionedThroughput: { readCapacityUnits: props.readCapacity || 5, writeCapacityUnits: props.writeCapacity || 5 }
254270
});
255271
}
256272

273+
/**
274+
* Add a local secondary index of table.
275+
*
276+
* @param props the property of local secondary index
277+
*/
278+
public addLocalSecondaryIndex(props: LocalSecondaryIndexProps) {
279+
if (this.localSecondaryIndexes.length === 5) {
280+
// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-secondary-indexes
281+
throw new RangeError('a maximum number of local secondary index per table is 5');
282+
}
283+
284+
if (!this.tablePartitionKey) {
285+
throw new Error('a partition key of the table must be specified first through addPartitionKey()');
286+
}
287+
288+
this.validateIndexName(props.indexName);
289+
290+
// build key schema and projection for index
291+
const lsiKeySchema = this.buildIndexKeySchema(this.tablePartitionKey, props.sortKey);
292+
const lsiProjection = this.buildIndexProjection(props);
293+
294+
this.secondaryIndexNames.push(props.indexName);
295+
this.localSecondaryIndexes.push({
296+
indexName: props.indexName,
297+
keySchema: lsiKeySchema,
298+
projection: lsiProjection
299+
});
300+
}
301+
257302
public addReadAutoScaling(props: AutoScalingProps) {
258303
this.readScalingPolicyResource = this.buildAutoScaling(this.readScalingPolicyResource, 'Read', props);
259304
}
@@ -262,18 +307,41 @@ export class Table extends Construct {
262307
this.writeScalingPolicyResource = this.buildAutoScaling(this.writeScalingPolicyResource, 'Write', props);
263308
}
264309

310+
/**
311+
* Validate the table construct.
312+
*
313+
* @returns an array of validation error message
314+
*/
265315
public validate(): string[] {
266316
const errors = new Array<string>();
267-
if (!this.findKey(HASH_KEY_TYPE)) {
317+
318+
if (!this.tablePartitionKey) {
268319
errors.push('a partition key must be specified');
269320
}
321+
if (this.localSecondaryIndexes.length > 0 && !this.tableSortKey) {
322+
errors.push('a sort key of the table must be specified to add local secondary indexes');
323+
}
324+
270325
return errors;
271326
}
272327

328+
/**
329+
* Validate index name to check if a duplicate name already exists.
330+
*
331+
* @param indexName a name of global or local secondary index
332+
*/
333+
private validateIndexName(indexName: string) {
334+
if (this.secondaryIndexNames.includes(indexName)) {
335+
// a duplicate index name causes validation exception, status code 400, while trying to create CFN stack
336+
throw new Error(`a duplicate index name, ${indexName}, is not allowed`);
337+
}
338+
this.secondaryIndexNames.push(indexName);
339+
}
340+
273341
/**
274342
* Validate non-key attributes by checking limits within secondary index, which may vary in future.
275343
*
276-
* @param {string[]} nonKeyAttributes a list of non-key attribute names
344+
* @param nonKeyAttributes a list of non-key attribute names
277345
*/
278346
private validateNonKeyAttributes(nonKeyAttributes: string[]) {
279347
if (this.nonKeyAttributes.length + nonKeyAttributes.length > 20) {
@@ -313,17 +381,40 @@ export class Table extends Construct {
313381
}
314382

315383
private buildIndexKeySchema(partitionKey: Attribute, sortKey?: Attribute): dynamodb.TableResource.KeySchemaProperty[] {
384+
this.registerAttribute(partitionKey);
316385
const indexKeySchema: dynamodb.TableResource.KeySchemaProperty[] = [
317-
{attributeName: partitionKey.name, keyType: HASH_KEY_TYPE}
386+
{ attributeName: partitionKey.name, keyType: HASH_KEY_TYPE }
318387
];
319388

320389
if (sortKey) {
321-
indexKeySchema.push({attributeName: sortKey.name, keyType: RANGE_KEY_TYPE});
390+
this.registerAttribute(sortKey);
391+
indexKeySchema.push({ attributeName: sortKey.name, keyType: RANGE_KEY_TYPE });
322392
}
323393

324394
return indexKeySchema;
325395
}
326396

397+
private buildIndexProjection(props: SecondaryIndexProps): dynamodb.TableResource.ProjectionProperty {
398+
if (props.projectionType === ProjectionType.Include && !props.nonKeyAttributes) {
399+
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-projectionobject.html
400+
throw new Error(`non-key attributes should be specified when using ${ProjectionType.Include} projection type`);
401+
}
402+
403+
if (props.projectionType !== ProjectionType.Include && props.nonKeyAttributes) {
404+
// this combination causes validation exception, status code 400, while trying to create CFN stack
405+
throw new Error(`non-key attributes should not be specified when not using ${ProjectionType.Include} projection type`);
406+
}
407+
408+
if (props.nonKeyAttributes) {
409+
this.validateNonKeyAttributes(props.nonKeyAttributes);
410+
}
411+
412+
return {
413+
projectionType: props.projectionType ? props.projectionType : ProjectionType.All,
414+
nonKeyAttributes: props.nonKeyAttributes ? props.nonKeyAttributes : undefined
415+
};
416+
}
417+
327418
private buildAutoScaling(scalingPolicyResource: applicationautoscaling.ScalingPolicyResource | undefined,
328419
scalingType: string,
329420
props: AutoScalingProps) {
@@ -411,7 +502,7 @@ export class Table extends Construct {
411502
/**
412503
* Register the key attribute of table or secondary index to assemble attribute definitions of TableResourceProps.
413504
*
414-
* @param {Attribute} attribute the key attribute of table or secondary index
505+
* @param attribute the key attribute of table or secondary index
415506
*/
416507
private registerAttribute(attribute: Attribute) {
417508
const name = attribute.name;

0 commit comments

Comments
 (0)