Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support DynamoDB GSI #760

Merged
merged 1 commit into from
Sep 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 146 additions & 16 deletions packages/@aws-cdk/aws-dynamodb/lib/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ import { cloudformation as dynamodb } from './dynamodb.generated';
const HASH_KEY_TYPE = 'HASH';
const RANGE_KEY_TYPE = 'RANGE';

export interface Attribute {
/**
* The name of an attribute.
*/
name: string;

/**
* The data type of an attribute.
*/
type: AttributeType;
}

export interface TableProps {
/**
* The read capacity for the table. Careful if you add Global Secondary Indexes, as
Expand Down Expand Up @@ -66,16 +78,46 @@ export interface TableProps {
writeAutoScaling?: AutoScalingProps;
}

export interface Attribute {
export interface SecondaryIndexProps {
/**
* The name of an attribute.
* The name of the secondary index.
*/
name: string;
indexName: string;

/**
* The data type of an attribute.
* The attribute of a partition key for the secondary index.
*/
type: AttributeType;
partitionKey: Attribute;

/**
* The attribute of a sort key for the secondary index.
* @default undefined
*/
sortKey?: Attribute;

/**
* The set of attributes that are projected into the secondary index.
* @default ALL
*/
projectionType?: ProjectionType;

/**
* The non-key attributes that are projected into the secondary index.
* @default undefined
*/
nonKeyAttributes?: string[];

/**
* The read capacity for the secondary index.
* @default 5
*/
readCapacity?: number;

/**
* The write capacity for the secondary index.
* @default 5
*/
writeCapacity?: number;
}

/* tslint:disable:max-line-length */
Expand Down Expand Up @@ -126,22 +168,23 @@ export class Table extends Construct {

private readonly keySchema = new Array<dynamodb.TableResource.KeySchemaProperty>();
private readonly attributeDefinitions = new Array<dynamodb.TableResource.AttributeDefinitionProperty>();
private readonly globalSecondaryIndexes = new Array<dynamodb.TableResource.GlobalSecondaryIndexProperty>();

private readonly nonKeyAttributes: string[] = [];

private readScalingPolicyResource?: applicationautoscaling.ScalingPolicyResource;
private writeScalingPolicyResource?: applicationautoscaling.ScalingPolicyResource;

constructor(parent: Construct, name: string, props: TableProps = {}) {
super(parent, name);

const readCapacityUnits = props.readCapacity || 5;
const writeCapacityUnits = props.writeCapacity || 5;

this.table = new dynamodb.TableResource(this, 'Resource', {
tableName: props.tableName,
keySchema: this.keySchema,
attributeDefinitions: this.attributeDefinitions,
globalSecondaryIndexes: this.globalSecondaryIndexes,
pointInTimeRecoverySpecification: props.pitrEnabled ? { pointInTimeRecoveryEnabled: props.pitrEnabled } : undefined,
provisionedThroughput: { readCapacityUnits, writeCapacityUnits },
provisionedThroughput: { readCapacityUnits: props.readCapacity || 5, writeCapacityUnits: props.writeCapacity || 5 },
sseSpecification: props.sseEnabled ? { sseEnabled: props.sseEnabled } : undefined,
streamSpecification: props.streamSpecification ? { streamViewType: props.streamSpecification } : undefined,
timeToLiveSpecification: props.ttlAttributeName ? { attributeName: props.ttlAttributeName, enabled: true } : undefined
Expand All @@ -163,15 +206,54 @@ export class Table extends Construct {
}

public addPartitionKey(attribute: Attribute): this {
this.addKey(attribute.name, attribute.type, HASH_KEY_TYPE);
this.addKey(attribute, HASH_KEY_TYPE);
return this;
}

public addSortKey(attribute: Attribute): this {
this.addKey(attribute.name, attribute.type, RANGE_KEY_TYPE);
this.addKey(attribute, RANGE_KEY_TYPE);
return this;
}

public addGlobalSecondaryIndex(props: SecondaryIndexProps) {
if (this.globalSecondaryIndexes.length === 5) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-secondary-indexes
throw new RangeError('a maximum number of global secondary index per table is 5');
}

if (props.projectionType === ProjectionType.Include && !props.nonKeyAttributes) {
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-projectionobject.html
throw new Error(`non-key attributes should be specified when using ${ProjectionType.Include} projection type`);
}

if (props.projectionType !== ProjectionType.Include && props.nonKeyAttributes) {
// this combination causes validation exception, status code 400, while trying to create CFN stack
throw new Error(`non-key attributes should not be specified when not using ${ProjectionType.Include} projection type`);
}

// build key schema for index
const gsiKeySchema = this.buildIndexKeySchema(props.partitionKey, props.sortKey);

// register attribute to check if a given configuration is valid
this.registerAttribute(props.partitionKey);
if (props.sortKey) {
this.registerAttribute(props.sortKey);
}
if (props.nonKeyAttributes) {
this.validateNonKeyAttributes(props.nonKeyAttributes);
}

this.globalSecondaryIndexes.push({
indexName: props.indexName,
keySchema: gsiKeySchema,
projection: {
projectionType: props.projectionType ? props.projectionType : ProjectionType.All,
nonKeyAttributes: props.nonKeyAttributes ? props.nonKeyAttributes : undefined
},
provisionedThroughput: { readCapacityUnits: props.readCapacity || 5, writeCapacityUnits: props.writeCapacity || 5 }
});
}

public addReadAutoScaling(props: AutoScalingProps) {
this.readScalingPolicyResource = this.buildAutoScaling(this.readScalingPolicyResource, 'Read', props);
}
Expand All @@ -188,6 +270,29 @@ export class Table extends Construct {
return errors;
}

/**
* Validate non-key attributes by checking limits within secondary index, which may vary in future.
*
* @param {string[]} nonKeyAttributes a list of non-key attribute names
*/
private validateNonKeyAttributes(nonKeyAttributes: string[]) {
if (this.nonKeyAttributes.length + nonKeyAttributes.length > 20) {
// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-secondary-indexes
throw new RangeError('a maximum number of nonKeyAttributes across all of secondary indexes is 20');
}

// store all non-key attributes
this.nonKeyAttributes.push(...nonKeyAttributes);

// throw error if key attribute is part of non-key attributes
this.attributeDefinitions.forEach(keyAttribute => {
if (typeof keyAttribute.attributeName === 'string' && this.nonKeyAttributes.includes(keyAttribute.attributeName)) {
throw new Error(`a key attribute, ${keyAttribute.attributeName}, is part of a list of non-key attributes, ${this.nonKeyAttributes}` +
', which is not allowed since all key attributes are added automatically and this configuration causes stack creation failure');
}
});
}

private validateAutoScalingProps(props: AutoScalingProps) {
if (props.targetValue < 10 || props.targetValue > 90) {
throw new RangeError("scalingTargetValue for predefined metric type DynamoDBReadCapacityUtilization/"
Expand All @@ -207,6 +312,18 @@ export class Table extends Construct {
}
}

private buildIndexKeySchema(partitionKey: Attribute, sortKey?: Attribute): dynamodb.TableResource.KeySchemaProperty[] {
const indexKeySchema: dynamodb.TableResource.KeySchemaProperty[] = [
{attributeName: partitionKey.name, keyType: HASH_KEY_TYPE}
];

if (sortKey) {
indexKeySchema.push({attributeName: sortKey.name, keyType: RANGE_KEY_TYPE});
}

return indexKeySchema;
}

private buildAutoScaling(scalingPolicyResource: applicationautoscaling.ScalingPolicyResource | undefined,
scalingType: string,
props: AutoScalingProps) {
Expand Down Expand Up @@ -278,20 +395,27 @@ export class Table extends Construct {
return this.keySchema.find(prop => prop.keyType === keyType);
}

private addKey(name: string, type: AttributeType, keyType: string) {
private addKey(attribute: Attribute, keyType: string) {
const existingProp = this.findKey(keyType);
if (existingProp) {
throw new Error(`Unable to set ${name} as a ${keyType} key, because ${existingProp.attributeName} is a ${keyType} key`);
throw new Error(`Unable to set ${attribute.name} as a ${keyType} key, because ${existingProp.attributeName} is a ${keyType} key`);
}
this.registerAttribute(name, type);
this.registerAttribute(attribute);
this.keySchema.push({
attributeName: name,
attributeName: attribute.name,
keyType
});
return this;
}

private registerAttribute(name: string, type: AttributeType) {
/**
* Register the key attribute of table or secondary index to assemble attribute definitions of TableResourceProps.
*
* @param {Attribute} attribute the key attribute of table or secondary index
*/
private registerAttribute(attribute: Attribute) {
const name = attribute.name;
const type = attribute.type;
const existingDef = this.attributeDefinitions.find(def => def.attributeName === name);
if (existingDef && existingDef.attributeType !== type) {
throw new Error(`Unable to specify ${name} as ${type} because it was already defined as ${existingDef.attributeType}`);
Expand All @@ -311,6 +435,12 @@ export enum AttributeType {
String = 'S',
}

export enum ProjectionType {
KeysOnly = 'KEYS_ONLY',
Include = 'INCLUDE',
All = 'ALL'
}

/**
* When an item in the table is modified, StreamViewType determines what information
* is written to the stream for this table. Valid values for StreamViewType are:
Expand Down
Loading