From a67b2d99266ded0f640e329ceaed83ac37703713 Mon Sep 17 00:00:00 2001 From: Jungseok Lee Date: Fri, 5 Oct 2018 01:39:54 -0700 Subject: [PATCH] feat(aws-dynamodB): support Local Secondary Indexes (#825) Adds support for specifying Local Secondary Indexes on DynamoDB tables. --- packages/@aws-cdk/aws-dynamodb/lib/table.ts | 171 +++++++--- .../test/integ.dynamodb.expected.json | 205 ++++++++++-- .../aws-dynamodb/test/integ.dynamodb.ts | 82 ++++- .../aws-dynamodb/test/test.dynamodb.ts | 292 ++++++++++++++++-- 4 files changed, 656 insertions(+), 94 deletions(-) diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index db2483a8529f2..3fa09a9a52f83 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -85,41 +85,50 @@ export interface SecondaryIndexProps { indexName: string; /** - * The attribute of a partition key for the secondary index. + * The set of attributes that are projected into the secondary index. + * @default ALL */ - partitionKey: Attribute; + projectionType?: ProjectionType; /** - * The attribute of a sort key for the secondary index. + * The non-key attributes that are projected into the secondary index. * @default undefined */ - sortKey?: Attribute; + nonKeyAttributes?: string[]; +} +export interface GlobalSecondaryIndexProps extends SecondaryIndexProps { /** - * The set of attributes that are projected into the secondary index. - * @default ALL + * The attribute of a partition key for the global secondary index. */ - projectionType?: ProjectionType; + partitionKey: Attribute; /** - * The non-key attributes that are projected into the secondary index. + * The attribute of a sort key for the global secondary index. * @default undefined */ - nonKeyAttributes?: string[]; + sortKey?: Attribute; /** - * The read capacity for the secondary index. + * The read capacity for the global secondary index. * @default 5 */ readCapacity?: number; /** - * The write capacity for the secondary index. + * The write capacity for the global secondary index. * @default 5 */ writeCapacity?: number; } +export interface LocalSecondaryIndexProps extends SecondaryIndexProps { + /** + * The attribute of a sort key for the local secondary index. + */ + sortKey: Attribute; +} + /* tslint:disable:max-line-length */ export interface AutoScalingProps { /** @@ -169,9 +178,14 @@ export class Table extends Construct { private readonly keySchema = new Array(); private readonly attributeDefinitions = new Array(); private readonly globalSecondaryIndexes = new Array(); + private readonly localSecondaryIndexes = new Array(); + private readonly secondaryIndexNames: string[] = []; private readonly nonKeyAttributes: string[] = []; + private tablePartitionKey: Attribute | undefined = undefined; + private tableSortKey: Attribute | undefined = undefined; + private readScalingPolicyResource?: applicationautoscaling.ScalingPolicyResource; private writeScalingPolicyResource?: applicationautoscaling.ScalingPolicyResource; @@ -183,6 +197,7 @@ export class Table extends Construct { keySchema: this.keySchema, attributeDefinitions: this.attributeDefinitions, globalSecondaryIndexes: this.globalSecondaryIndexes, + localSecondaryIndexes: this.localSecondaryIndexes, pointInTimeRecoverySpecification: props.pitrEnabled ? { pointInTimeRecoveryEnabled: props.pitrEnabled } : undefined, provisionedThroughput: { readCapacityUnits: props.readCapacity || 5, writeCapacityUnits: props.writeCapacity || 5 }, sseSpecification: props.sseEnabled ? { sseEnabled: props.sseEnabled } : undefined, @@ -205,55 +220,85 @@ export class Table extends Construct { } } + /** + * Add a partition key of table. + * + * @param attribute the partition key attribute of table + * @returns a reference to this object so that method calls can be chained together + */ public addPartitionKey(attribute: Attribute): this { this.addKey(attribute, HASH_KEY_TYPE); + this.tablePartitionKey = attribute; return this; } + /** + * Add a sort key of table. + * + * @param attribute the sort key of table + * @returns a reference to this object so that method calls can be chained together + */ public addSortKey(attribute: Attribute): this { this.addKey(attribute, RANGE_KEY_TYPE); + this.tableSortKey = attribute; return this; } - public addGlobalSecondaryIndex(props: SecondaryIndexProps) { + /** + * Add a global secondary index of table. + * + * @param props the property of global secondary index + */ + public addGlobalSecondaryIndex(props: GlobalSecondaryIndexProps) { if (this.globalSecondaryIndexes.length === 5) { // 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`); - } + this.validateIndexName(props.indexName); - // build key schema for index + // build key schema and projection for index const gsiKeySchema = this.buildIndexKeySchema(props.partitionKey, props.sortKey); + const gsiProjection = this.buildIndexProjection(props); - // 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.secondaryIndexNames.push(props.indexName); this.globalSecondaryIndexes.push({ indexName: props.indexName, keySchema: gsiKeySchema, - projection: { - projectionType: props.projectionType ? props.projectionType : ProjectionType.All, - nonKeyAttributes: props.nonKeyAttributes ? props.nonKeyAttributes : undefined - }, + projection: gsiProjection, provisionedThroughput: { readCapacityUnits: props.readCapacity || 5, writeCapacityUnits: props.writeCapacity || 5 } }); } + /** + * Add a local secondary index of table. + * + * @param props the property of local secondary index + */ + public addLocalSecondaryIndex(props: LocalSecondaryIndexProps) { + if (this.localSecondaryIndexes.length === 5) { + // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-secondary-indexes + throw new RangeError('a maximum number of local secondary index per table is 5'); + } + + if (!this.tablePartitionKey) { + throw new Error('a partition key of the table must be specified first through addPartitionKey()'); + } + + this.validateIndexName(props.indexName); + + // build key schema and projection for index + const lsiKeySchema = this.buildIndexKeySchema(this.tablePartitionKey, props.sortKey); + const lsiProjection = this.buildIndexProjection(props); + + this.secondaryIndexNames.push(props.indexName); + this.localSecondaryIndexes.push({ + indexName: props.indexName, + keySchema: lsiKeySchema, + projection: lsiProjection + }); + } + public addReadAutoScaling(props: AutoScalingProps) { this.readScalingPolicyResource = this.buildAutoScaling(this.readScalingPolicyResource, 'Read', props); } @@ -262,18 +307,41 @@ export class Table extends Construct { this.writeScalingPolicyResource = this.buildAutoScaling(this.writeScalingPolicyResource, 'Write', props); } + /** + * Validate the table construct. + * + * @returns an array of validation error message + */ public validate(): string[] { const errors = new Array(); - if (!this.findKey(HASH_KEY_TYPE)) { + + if (!this.tablePartitionKey) { errors.push('a partition key must be specified'); } + if (this.localSecondaryIndexes.length > 0 && !this.tableSortKey) { + errors.push('a sort key of the table must be specified to add local secondary indexes'); + } + return errors; } + /** + * Validate index name to check if a duplicate name already exists. + * + * @param indexName a name of global or local secondary index + */ + private validateIndexName(indexName: string) { + if (this.secondaryIndexNames.includes(indexName)) { + // a duplicate index name causes validation exception, status code 400, while trying to create CFN stack + throw new Error(`a duplicate index name, ${indexName}, is not allowed`); + } + this.secondaryIndexNames.push(indexName); + } + /** * 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 + * @param nonKeyAttributes a list of non-key attribute names */ private validateNonKeyAttributes(nonKeyAttributes: string[]) { if (this.nonKeyAttributes.length + nonKeyAttributes.length > 20) { @@ -313,17 +381,40 @@ export class Table extends Construct { } private buildIndexKeySchema(partitionKey: Attribute, sortKey?: Attribute): dynamodb.TableResource.KeySchemaProperty[] { + this.registerAttribute(partitionKey); const indexKeySchema: dynamodb.TableResource.KeySchemaProperty[] = [ - {attributeName: partitionKey.name, keyType: HASH_KEY_TYPE} + { attributeName: partitionKey.name, keyType: HASH_KEY_TYPE } ]; if (sortKey) { - indexKeySchema.push({attributeName: sortKey.name, keyType: RANGE_KEY_TYPE}); + this.registerAttribute(sortKey); + indexKeySchema.push({ attributeName: sortKey.name, keyType: RANGE_KEY_TYPE }); } return indexKeySchema; } + private buildIndexProjection(props: SecondaryIndexProps): dynamodb.TableResource.ProjectionProperty { + 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`); + } + + if (props.nonKeyAttributes) { + this.validateNonKeyAttributes(props.nonKeyAttributes); + } + + return { + projectionType: props.projectionType ? props.projectionType : ProjectionType.All, + nonKeyAttributes: props.nonKeyAttributes ? props.nonKeyAttributes : undefined + }; + } + private buildAutoScaling(scalingPolicyResource: applicationautoscaling.ScalingPolicyResource | undefined, scalingType: string, props: AutoScalingProps) { @@ -411,7 +502,7 @@ export class Table extends Construct { /** * 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 + * @param attribute the key attribute of table or secondary index */ private registerAttribute(attribute: Attribute) { const name = attribute.name; diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.expected.json b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.expected.json index c24acd147d433..61fc25b5d6030 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.expected.json +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.expected.json @@ -1,6 +1,29 @@ { "Resources": { "TableCD117FA1": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + }, + "AttributeDefinitions": [ + { + "AttributeName": "hashKey", + "AttributeType": "S" + } + ], + "GlobalSecondaryIndexes": [], + "LocalSecondaryIndexes": [] + } + }, + "TableWithGlobalAndLocalSecondaryIndexBC540710": { "Type": "AWS::DynamoDB::Table", "Properties": { "KeySchema": [ @@ -33,11 +56,15 @@ { "AttributeName": "gsiSortKey", "AttributeType": "N" + }, + { + "AttributeName": "lsiSortKey", + "AttributeType": "N" } ], "GlobalSecondaryIndexes": [ { - "IndexName": "PartitionKeyOnly", + "IndexName": "GSI-PartitionKeyOnly", "KeySchema": [ { "AttributeName": "gsiHashKey", @@ -53,7 +80,7 @@ } }, { - "IndexName": "PartitionAndSortKeyWithReadAndWriteCapacity", + "IndexName": "GSI-PartitionAndSortKeyWithReadAndWriteCapacity", "KeySchema": [ { "AttributeName": "gsiHashKey", @@ -73,7 +100,7 @@ } }, { - "IndexName": "ProjectionTypeKeysOnly", + "IndexName": "GSI-ProjectionTypeKeysOnly", "KeySchema": [ { "AttributeName": "gsiHashKey", @@ -93,7 +120,7 @@ } }, { - "IndexName": "ProjectionTypeInclude", + "IndexName": "GSI-ProjectionTypeInclude", "KeySchema": [ { "AttributeName": "gsiHashKey", @@ -115,17 +142,7 @@ "G", "H", "I", - "J", - "K", - "L", - "M", - "N", - "O", - "P", - "Q", - "R", - "S", - "T" + "J" ], "ProjectionType": "INCLUDE" }, @@ -135,7 +152,7 @@ } }, { - "IndexName": "InverseTableKeySchema", + "IndexName": "GSI-InverseTableKeySchema", "KeySchema": [ { "AttributeName": "sortKey", @@ -155,6 +172,84 @@ } } ], + "LocalSecondaryIndexes": [ + { + "IndexName": "LSI-PartitionAndTableSortKey", + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "lsiSortKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + }, + { + "IndexName": "LSI-PartitionAndSortKey", + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "sortKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + }, + { + "IndexName": "LSI-ProjectionTypeKeysOnly", + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "lsiSortKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "KEYS_ONLY" + } + }, + { + "IndexName": "LSI-ProjectionTypeInclude", + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "lsiSortKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "NonKeyAttributes": [ + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T" + ], + "ProjectionType": "INCLUDE" + } + } + ], "PointInTimeRecoverySpecification": { "PointInTimeRecoveryEnabled": true }, @@ -170,13 +265,61 @@ } } }, - "TableWithoutSecondaryIndex5A9C91D2": { + "TableWithGlobalSecondaryIndexCC8E841E": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + }, + "AttributeDefinitions": [ + { + "AttributeName": "hashKey", + "AttributeType": "S" + }, + { + "AttributeName": "gsiHashKey", + "AttributeType": "S" + } + ], + "GlobalSecondaryIndexes": [ + { + "IndexName": "GSI-PartitionKeyOnly", + "KeySchema": [ + { + "AttributeName": "gsiHashKey", + "KeyType": "HASH" + } + ], + "Projection": { + "ProjectionType": "ALL" + }, + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + } + } + ], + "LocalSecondaryIndexes": [] + } + }, + "TableWithLocalSecondaryIndex4DA3D08F": { "Type": "AWS::DynamoDB::Table", "Properties": { "KeySchema": [ { "AttributeName": "hashKey", "KeyType": "HASH" + }, + { + "AttributeName": "sortKey", + "KeyType": "RANGE" } ], "ProvisionedThroughput": { @@ -187,9 +330,35 @@ { "AttributeName": "hashKey", "AttributeType": "S" + }, + { + "AttributeName": "sortKey", + "AttributeType": "N" + }, + { + "AttributeName": "lsiSortKey", + "AttributeType": "N" } ], - "GlobalSecondaryIndexes": [] + "GlobalSecondaryIndexes": [], + "LocalSecondaryIndexes": [ + { + "IndexName": "LSI-PartitionAndSortKey", + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "lsiSortKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + } + ] } } } diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.ts b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.ts index 7f86844fcedb4..922749aa8c8b1 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.ts @@ -5,66 +5,116 @@ import { Attribute, AttributeType, ProjectionType, StreamViewType, Table } from const STACK_NAME = 'aws-cdk-dynamodb'; // DynamoDB table parameters +const TABLE = 'Table'; +const TABLE_WITH_GLOBAL_AND_LOCAL_SECONDARY_INDEX = 'TableWithGlobalAndLocalSecondaryIndex'; +const TABLE_WITH_GLOBAL_SECONDARY_INDEX = 'TableWithGlobalSecondaryIndex'; +const TABLE_WITH_LOCAL_SECONDARY_INDEX = 'TableWithLocalSecondaryIndex'; const TABLE_PARTITION_KEY: Attribute = { name: 'hashKey', type: AttributeType.String }; const TABLE_SORT_KEY: Attribute = { name: 'sortKey', type: AttributeType.Number }; // DynamoDB global secondary index parameters -const GSI_TEST_CASE_1 = 'PartitionKeyOnly'; -const GSI_TEST_CASE_2 = 'PartitionAndSortKeyWithReadAndWriteCapacity'; -const GSI_TEST_CASE_3 = 'ProjectionTypeKeysOnly'; -const GSI_TEST_CASE_4 = 'ProjectionTypeInclude'; -const GSI_TEST_CASE_5 = 'InverseTableKeySchema'; +const GSI_TEST_CASE_1 = 'GSI-PartitionKeyOnly'; +const GSI_TEST_CASE_2 = 'GSI-PartitionAndSortKeyWithReadAndWriteCapacity'; +const GSI_TEST_CASE_3 = 'GSI-ProjectionTypeKeysOnly'; +const GSI_TEST_CASE_4 = 'GSI-ProjectionTypeInclude'; +const GSI_TEST_CASE_5 = 'GSI-InverseTableKeySchema'; const GSI_PARTITION_KEY: Attribute = { name: 'gsiHashKey', type: AttributeType.String }; const GSI_SORT_KEY: Attribute = { name: 'gsiSortKey', type: AttributeType.Number }; const GSI_NON_KEY: string[] = []; -for (let i = 0; i < 20; i++) { // 'A' to 'T' +for (let i = 0; i < 10; i++) { // 'A' to 'J' GSI_NON_KEY.push(String.fromCharCode(65 + i)); } +// DynamoDB local secondary index parameters +const LSI_TEST_CASE_1 = 'LSI-PartitionAndSortKey'; +const LSI_TEST_CASE_2 = 'LSI-PartitionAndTableSortKey'; +const LSI_TEST_CASE_3 = 'LSI-ProjectionTypeKeysOnly'; +const LSI_TEST_CASE_4 = 'LSI-ProjectionTypeInclude'; +const LSI_SORT_KEY: Attribute = { name: 'lsiSortKey', type: AttributeType.Number }; +const LSI_NON_KEY: string[] = []; +for (let i = 0; i < 10; i++) { // 'K' to 'T' + LSI_NON_KEY.push(String.fromCharCode(75 + i)); +} + const app = new App(process.argv); const stack = new Stack(app, STACK_NAME); -const table = new Table(stack, 'Table', { +const table = new Table(stack, TABLE, {}); +table.addPartitionKey(TABLE_PARTITION_KEY); + +const tableWithGlobalAndLocalSecondaryIndex = new Table(stack, TABLE_WITH_GLOBAL_AND_LOCAL_SECONDARY_INDEX, { pitrEnabled: true, sseEnabled: true, streamSpecification: StreamViewType.KeysOnly, ttlAttributeName: 'timeToLive' }); -table.addPartitionKey(TABLE_PARTITION_KEY); -table.addSortKey(TABLE_SORT_KEY); -table.addGlobalSecondaryIndex({ +tableWithGlobalAndLocalSecondaryIndex.addPartitionKey(TABLE_PARTITION_KEY); +tableWithGlobalAndLocalSecondaryIndex.addSortKey(TABLE_SORT_KEY); +tableWithGlobalAndLocalSecondaryIndex.addGlobalSecondaryIndex({ indexName: GSI_TEST_CASE_1, partitionKey: GSI_PARTITION_KEY, }); -table.addGlobalSecondaryIndex({ +tableWithGlobalAndLocalSecondaryIndex.addGlobalSecondaryIndex({ indexName: GSI_TEST_CASE_2, partitionKey: GSI_PARTITION_KEY, sortKey: GSI_SORT_KEY, readCapacity: 10, writeCapacity: 10, }); -table.addGlobalSecondaryIndex({ +tableWithGlobalAndLocalSecondaryIndex.addGlobalSecondaryIndex({ indexName: GSI_TEST_CASE_3, partitionKey: GSI_PARTITION_KEY, sortKey: GSI_SORT_KEY, projectionType: ProjectionType.KeysOnly, }); -table.addGlobalSecondaryIndex({ +tableWithGlobalAndLocalSecondaryIndex.addGlobalSecondaryIndex({ indexName: GSI_TEST_CASE_4, partitionKey: GSI_PARTITION_KEY, sortKey: GSI_SORT_KEY, projectionType: ProjectionType.Include, nonKeyAttributes: GSI_NON_KEY }); -table.addGlobalSecondaryIndex({ +tableWithGlobalAndLocalSecondaryIndex.addGlobalSecondaryIndex({ indexName: GSI_TEST_CASE_5, partitionKey: TABLE_SORT_KEY, sortKey: TABLE_PARTITION_KEY, }); -const tableWithoutSecondaryIndex = new Table(stack, 'TableWithoutSecondaryIndex', {}); -tableWithoutSecondaryIndex.addPartitionKey(TABLE_PARTITION_KEY); +tableWithGlobalAndLocalSecondaryIndex.addLocalSecondaryIndex({ + indexName: LSI_TEST_CASE_2, + sortKey: LSI_SORT_KEY +}); +tableWithGlobalAndLocalSecondaryIndex.addLocalSecondaryIndex({ + indexName: LSI_TEST_CASE_1, + sortKey: TABLE_SORT_KEY +}); +tableWithGlobalAndLocalSecondaryIndex.addLocalSecondaryIndex({ + indexName: LSI_TEST_CASE_3, + sortKey: LSI_SORT_KEY, + projectionType: ProjectionType.KeysOnly +}); +tableWithGlobalAndLocalSecondaryIndex.addLocalSecondaryIndex({ + indexName: LSI_TEST_CASE_4, + sortKey: LSI_SORT_KEY, + projectionType: ProjectionType.Include, + nonKeyAttributes: LSI_NON_KEY +}); + +const tableWithGlobalSecondaryIndex = new Table(stack, TABLE_WITH_GLOBAL_SECONDARY_INDEX, {}); +tableWithGlobalSecondaryIndex.addPartitionKey(TABLE_PARTITION_KEY); +tableWithGlobalSecondaryIndex.addGlobalSecondaryIndex({ + indexName: GSI_TEST_CASE_1, + partitionKey: GSI_PARTITION_KEY +}); + +const tableWithLocalSecondaryIndex = new Table(stack, TABLE_WITH_LOCAL_SECONDARY_INDEX, {}); +tableWithLocalSecondaryIndex.addPartitionKey(TABLE_PARTITION_KEY); +tableWithLocalSecondaryIndex.addSortKey(TABLE_SORT_KEY); +tableWithLocalSecondaryIndex.addLocalSecondaryIndex({ + indexName: LSI_TEST_CASE_1, + sortKey: LSI_SORT_KEY +}); process.stdout.write(app.run()); diff --git a/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts b/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts index 731dddd8ecea0..c6850b447a3a0 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/test.dynamodb.ts @@ -1,6 +1,14 @@ import { App, Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; -import { Attribute, AttributeType, ProjectionType, SecondaryIndexProps, StreamViewType, Table } from '../lib'; +import { + Attribute, + AttributeType, + GlobalSecondaryIndexProps, + LocalSecondaryIndexProps, + ProjectionType, + StreamViewType, + Table +} from '../lib'; // CDK parameters const STACK_NAME = 'MyStack'; @@ -19,7 +27,7 @@ const GSI_NON_KEY = 'gsiNonKey'; function* GSI_GENERATOR() { let n = 0; while (true) { - const globalSecondaryIndexProps: SecondaryIndexProps = { + const globalSecondaryIndexProps: GlobalSecondaryIndexProps = { indexName: `${GSI_NAME}${n}`, partitionKey: { name: `${GSI_PARTITION_KEY.name}${n}`, type: GSI_PARTITION_KEY.type } }; @@ -27,10 +35,26 @@ function* GSI_GENERATOR() { n++; } } -function* GSI_NON_KEY_ATTRIBUTE_GENERATOR() { +function* NON_KEY_ATTRIBUTE_GENERATOR(nonKeyPrefix: string) { let n = 0; while (true) { - yield `${GSI_NON_KEY}${n}`; + yield `${nonKeyPrefix}${n}`; + n++; + } +} + +// DynamoDB local secondary index parameters +const LSI_NAME = 'MyLSI'; +const LSI_SORT_KEY: Attribute = { name: 'lsiSortKey', type: AttributeType.Number }; +const LSI_NON_KEY = 'lsiNonKey'; +function* LSI_GENERATOR() { + let n = 0; + while (true) { + const localSecondaryIndexProps: LocalSecondaryIndexProps = { + indexName: `${LSI_NAME}${n}`, + sortKey: { name : `${LSI_SORT_KEY.name}${n}`, type: LSI_SORT_KEY.type } + }; + yield localSecondaryIndexProps; n++; } } @@ -58,7 +82,8 @@ export = { AttributeDefinitions: [{ AttributeName: 'hashKey', AttributeType: 'S' }], KeySchema: [{ AttributeName: 'hashKey', KeyType: 'HASH' }], ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 }, - GlobalSecondaryIndexes: [] + GlobalSecondaryIndexes: [], + LocalSecondaryIndexes: [] } } } @@ -88,7 +113,8 @@ export = { { AttributeName: 'sortKey', KeyType: 'RANGE' } ], ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 }, - GlobalSecondaryIndexes: [] + GlobalSecondaryIndexes: [], + LocalSecondaryIndexes: [] } } } @@ -118,7 +144,8 @@ export = { { AttributeName: 'sortKey', KeyType: 'RANGE' } ], ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 }, - GlobalSecondaryIndexes: [] + GlobalSecondaryIndexes: [], + LocalSecondaryIndexes: [] } } } @@ -148,7 +175,8 @@ export = { { AttributeName: 'sortKey', KeyType: 'RANGE' } ], ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 }, - GlobalSecondaryIndexes: [] + GlobalSecondaryIndexes: [], + LocalSecondaryIndexes: [] } } } @@ -178,7 +206,8 @@ export = { { AttributeName: 'sortKey', KeyType: 'RANGE' } ], ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 }, - GlobalSecondaryIndexes: [] + GlobalSecondaryIndexes: [], + LocalSecondaryIndexes: [] } } } @@ -208,7 +237,8 @@ export = { { AttributeName: 'sortKey', KeyType: 'RANGE' } ], ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 }, - GlobalSecondaryIndexes: [] + GlobalSecondaryIndexes: [], + LocalSecondaryIndexes: [] } } } @@ -245,6 +275,7 @@ export = { ], ProvisionedThroughput: { ReadCapacityUnits: 42, WriteCapacityUnits: 1337 }, GlobalSecondaryIndexes: [], + LocalSecondaryIndexes: [], TableName: 'MyTable' } } @@ -277,6 +308,7 @@ export = { ], ProvisionedThroughput: { ReadCapacityUnits: 42, WriteCapacityUnits: 1337 }, GlobalSecondaryIndexes: [], + LocalSecondaryIndexes: [], AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, { AttributeName: 'sortKey', AttributeType: 'N' } @@ -314,6 +346,7 @@ export = { ], ProvisionedThroughput: { ReadCapacityUnits: 42, WriteCapacityUnits: 1337 }, GlobalSecondaryIndexes: [], + LocalSecondaryIndexes: [], AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, { AttributeName: 'sortKey', AttributeType: 'N' } @@ -362,6 +395,7 @@ export = { WriteCapacityUnits: 1337 }, GlobalSecondaryIndexes: [], + LocalSecondaryIndexes: [], PointInTimeRecoverySpecification: { PointInTimeRecoveryEnabled: true }, SSESpecification: { SSEEnabled: true }, StreamSpecification: { StreamViewType: 'KEYS_ONLY' }, @@ -412,7 +446,8 @@ export = { Projection: { ProjectionType: 'ALL' }, ProvisionedThroughput: { ReadCapacityUnits: 42, WriteCapacityUnits: 1337 } } - ] + ], + LocalSecondaryIndexes: [], } } } @@ -462,7 +497,8 @@ export = { Projection: { ProjectionType: 'ALL' }, ProvisionedThroughput: { ReadCapacityUnits: 42, WriteCapacityUnits: 1337 } } - ] + ], + LocalSecondaryIndexes: [], } } } @@ -510,7 +546,8 @@ export = { Projection: { ProjectionType: 'KEYS_ONLY' }, ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 } } - ] + ], + LocalSecondaryIndexes: [], } } } @@ -524,7 +561,7 @@ export = { const table = new Table(app.stack, CONSTRUCT_NAME) .addPartitionKey(TABLE_PARTITION_KEY) .addSortKey(TABLE_SORT_KEY); - const gsiNonKeyAttributeGenerator = GSI_NON_KEY_ATTRIBUTE_GENERATOR(); + const gsiNonKeyAttributeGenerator = NON_KEY_ATTRIBUTE_GENERATOR(GSI_NON_KEY); table.addGlobalSecondaryIndex({ indexName: GSI_NAME, partitionKey: GSI_PARTITION_KEY, @@ -562,7 +599,8 @@ export = { Projection: { NonKeyAttributes: ['gsiNonKey0', 'gsiNonKey1'], ProjectionType: 'INCLUDE' }, ProvisionedThroughput: { ReadCapacityUnits: 42, WriteCapacityUnits: 1337 } } - ] + ], + LocalSecondaryIndexes: [], } } } @@ -592,7 +630,7 @@ export = { const table = new Table(app.stack, CONSTRUCT_NAME) .addPartitionKey(TABLE_PARTITION_KEY) .addSortKey(TABLE_SORT_KEY); - const gsiNonKeyAttributeGenerator = GSI_NON_KEY_ATTRIBUTE_GENERATOR(); + const gsiNonKeyAttributeGenerator = NON_KEY_ATTRIBUTE_GENERATOR(GSI_NON_KEY); test.throws(() => table.addGlobalSecondaryIndex({ indexName: GSI_NAME, @@ -608,7 +646,7 @@ export = { const table = new Table(app.stack, CONSTRUCT_NAME) .addPartitionKey(TABLE_PARTITION_KEY) .addSortKey(TABLE_SORT_KEY); - const gsiNonKeyAttributeGenerator = GSI_NON_KEY_ATTRIBUTE_GENERATOR(); + const gsiNonKeyAttributeGenerator = NON_KEY_ATTRIBUTE_GENERATOR(GSI_NON_KEY); test.throws(() => table.addGlobalSecondaryIndex({ indexName: GSI_NAME, @@ -625,7 +663,7 @@ export = { const table = new Table(app.stack, CONSTRUCT_NAME) .addPartitionKey(TABLE_PARTITION_KEY) .addSortKey(TABLE_SORT_KEY); - const gsiNonKeyAttributeGenerator = GSI_NON_KEY_ATTRIBUTE_GENERATOR(); + const gsiNonKeyAttributeGenerator = NON_KEY_ATTRIBUTE_GENERATOR(GSI_NON_KEY); const gsiNonKeyAttributes: string[] = []; for (let i = 0; i < 21; i++) { gsiNonKeyAttributes.push(gsiNonKeyAttributeGenerator.next().value); @@ -731,7 +769,8 @@ export = { Projection: { ProjectionType: 'ALL' }, ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 } }, - ] + ], + LocalSecondaryIndexes: [], } } } @@ -791,7 +830,53 @@ export = { Projection: { ProjectionType: 'ALL' }, ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 } } - ] + ], + LocalSecondaryIndexes: [], + } + } + } + }); + + test.done(); + }, + + 'when adding a local secondary index with hash + range key'(test: Test) { + const app = new TestApp(); + new Table(app.stack, CONSTRUCT_NAME) + .addPartitionKey(TABLE_PARTITION_KEY) + .addSortKey(TABLE_SORT_KEY) + .addLocalSecondaryIndex({ + indexName: LSI_NAME, + sortKey: LSI_SORT_KEY, + }); + const template = app.synthesizeTemplate(); + + test.deepEqual(template, { + Resources: { + MyTable794EDED1: { + Type: 'AWS::DynamoDB::Table', + Properties: { + AttributeDefinitions: [ + { AttributeName: 'hashKey', AttributeType: 'S' }, + { AttributeName: 'sortKey', AttributeType: 'N' }, + { AttributeName: 'lsiSortKey', AttributeType: 'N' } + ], + KeySchema: [ + { AttributeName: 'hashKey', KeyType: 'HASH' }, + { AttributeName: 'sortKey', KeyType: 'RANGE' } + ], + ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 }, + GlobalSecondaryIndexes: [], + LocalSecondaryIndexes: [ + { + IndexName: 'MyLSI', + KeySchema: [ + { AttributeName: 'hashKey', KeyType: 'HASH' }, + { AttributeName: 'lsiSortKey', KeyType: 'RANGE' } + ], + Projection: { ProjectionType: 'ALL' }, + } + ], } } } @@ -800,6 +885,165 @@ export = { test.done(); }, + 'when adding a local secondary index with projection type KEYS_ONLY'(test: Test) { + const app = new TestApp(); + new Table(app.stack, CONSTRUCT_NAME) + .addPartitionKey(TABLE_PARTITION_KEY) + .addSortKey(TABLE_SORT_KEY) + .addLocalSecondaryIndex({ + indexName: LSI_NAME, + sortKey: LSI_SORT_KEY, + projectionType: ProjectionType.KeysOnly + }); + const template = app.synthesizeTemplate(); + + test.deepEqual(template, { + Resources: { + MyTable794EDED1: { + Type: 'AWS::DynamoDB::Table', + Properties: { + AttributeDefinitions: [ + { AttributeName: 'hashKey', AttributeType: 'S' }, + { AttributeName: 'sortKey', AttributeType: 'N' }, + { AttributeName: 'lsiSortKey', AttributeType: 'N' } + ], + KeySchema: [ + { AttributeName: 'hashKey', KeyType: 'HASH' }, + { AttributeName: 'sortKey', KeyType: 'RANGE' } + ], + ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 }, + GlobalSecondaryIndexes: [], + LocalSecondaryIndexes: [ + { + IndexName: 'MyLSI', + KeySchema: [ + { AttributeName: 'hashKey', KeyType: 'HASH' }, + { AttributeName: 'lsiSortKey', KeyType: 'RANGE' } + ], + Projection: { ProjectionType: 'KEYS_ONLY' }, + } + ], + } + } + } + }); + + test.done(); + }, + + 'when adding a local secondary index with projection type INCLUDE'(test: Test) { + const app = new TestApp(); + const table = new Table(app.stack, CONSTRUCT_NAME) + .addPartitionKey(TABLE_PARTITION_KEY) + .addSortKey(TABLE_SORT_KEY); + const lsiNonKeyAttributeGenerator = NON_KEY_ATTRIBUTE_GENERATOR(LSI_NON_KEY); + table.addLocalSecondaryIndex({ + indexName: LSI_NAME, + sortKey: LSI_SORT_KEY, + projectionType: ProjectionType.Include, + nonKeyAttributes: [ lsiNonKeyAttributeGenerator.next().value, lsiNonKeyAttributeGenerator.next().value ] + }); + + const template = app.synthesizeTemplate(); + + test.deepEqual(template, { + Resources: { + MyTable794EDED1: { + Type: 'AWS::DynamoDB::Table', + Properties: { + AttributeDefinitions: [ + { AttributeName: 'hashKey', AttributeType: 'S' }, + { AttributeName: 'sortKey', AttributeType: 'N' }, + { AttributeName: 'lsiSortKey', AttributeType: 'N' } + ], + KeySchema: [ + { AttributeName: 'hashKey', KeyType: 'HASH' }, + { AttributeName: 'sortKey', KeyType: 'RANGE' } + ], + ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 }, + GlobalSecondaryIndexes: [], + LocalSecondaryIndexes: [ + { + IndexName: 'MyLSI', + KeySchema: [ + { AttributeName: 'hashKey', KeyType: 'HASH' }, + { AttributeName: 'lsiSortKey', KeyType: 'RANGE' } + ], + Projection: { NonKeyAttributes: ['lsiNonKey0', 'lsiNonKey1'], ProjectionType: 'INCLUDE' }, + } + ], + } + } + } + }); + + test.done(); + }, + + 'error when adding more than 5 local secondary indexes'(test: Test) { + const app = new TestApp(); + const table = new Table(app.stack, CONSTRUCT_NAME) + .addPartitionKey(TABLE_PARTITION_KEY) + .addSortKey(TABLE_SORT_KEY); + const lsiGenerator = LSI_GENERATOR(); + for (let i = 0; i < 5; i++) { + table.addLocalSecondaryIndex(lsiGenerator.next().value); + } + + test.throws(() => table.addLocalSecondaryIndex(lsiGenerator.next().value), + /a maximum number of local secondary index per table is 5/); + + test.done(); + }, + + 'error when adding a local secondary index before specifying a partition key of the table'(test: Test) { + const app = new TestApp(); + const table = new Table(app.stack, CONSTRUCT_NAME) + .addSortKey(TABLE_SORT_KEY); + + test.throws(() => table.addLocalSecondaryIndex({ + indexName: LSI_NAME, + sortKey: LSI_SORT_KEY + }), /a partition key of the table must be specified first through addPartitionKey()/); + + test.done(); + }, + + 'error when adding a local secondary index with the name of a global secondary index'(test: Test) { + const app = new TestApp(); + const table = new Table(app.stack, CONSTRUCT_NAME) + .addPartitionKey(TABLE_PARTITION_KEY) + .addSortKey(TABLE_SORT_KEY); + table.addGlobalSecondaryIndex({ + indexName: 'SecondaryIndex', + partitionKey: GSI_PARTITION_KEY + }); + + test.throws(() => table.addLocalSecondaryIndex({ + indexName: 'SecondaryIndex', + sortKey: LSI_SORT_KEY + }), /a duplicate index name, SecondaryIndex, is not allowed/); + + test.done(); + }, + + 'error when validating construct if a local secondary index exists without a sort key of the table'(test: Test) { + const app = new TestApp(); + const table = new Table(app.stack, CONSTRUCT_NAME) + .addPartitionKey(TABLE_PARTITION_KEY); + table.addLocalSecondaryIndex({ + indexName: LSI_NAME, + sortKey: LSI_SORT_KEY + }); + + const errors = table.validate(); + + test.strictEqual(1, errors.length); + test.strictEqual('a sort key of the table must be specified to add local secondary indexes', errors[0]); + + test.done(); + }, + 'when specifying Read Auto Scaling'(test: Test) { const app = new TestApp(); const table = new Table(app.stack, CONSTRUCT_NAME, { @@ -828,6 +1072,7 @@ export = { { AttributeName: 'sortKey', KeyType: 'RANGE' } ], ProvisionedThroughput: { ReadCapacityUnits: 42, WriteCapacityUnits: 1337 }, GlobalSecondaryIndexes: [], + LocalSecondaryIndexes: [], AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, { AttributeName: 'sortKey', AttributeType: 'N' } ], @@ -909,6 +1154,7 @@ export = { { AttributeName: 'sortKey', KeyType: 'RANGE' } ], ProvisionedThroughput: { ReadCapacityUnits: 42, WriteCapacityUnits: 1337 }, GlobalSecondaryIndexes: [], + LocalSecondaryIndexes: [], AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, { AttributeName: 'sortKey', AttributeType: 'N' } ], @@ -1018,6 +1264,7 @@ export = { { AttributeName: 'sortKey', KeyType: 'RANGE' } ], ProvisionedThroughput: { ReadCapacityUnits: 42, WriteCapacityUnits: 1337 }, GlobalSecondaryIndexes: [], + LocalSecondaryIndexes: [], AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, { AttributeName: 'sortKey', AttributeType: 'N' } ], @@ -1098,6 +1345,7 @@ export = { { AttributeName: 'sortKey', KeyType: 'RANGE' } ], ProvisionedThroughput: { ReadCapacityUnits: 42, WriteCapacityUnits: 1337 }, GlobalSecondaryIndexes: [], + LocalSecondaryIndexes: [], AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, { AttributeName: 'sortKey', AttributeType: 'N' } ] } }, @@ -1301,6 +1549,7 @@ export = { { AttributeName: 'sortKey', KeyType: 'RANGE' } ], ProvisionedThroughput: { ReadCapacityUnits: 42, WriteCapacityUnits: 1337 }, GlobalSecondaryIndexes: [], + LocalSecondaryIndexes: [], AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, { AttributeName: 'sortKey', AttributeType: 'N' } ], @@ -1382,6 +1631,7 @@ export = { { AttributeName: 'sortKey', KeyType: 'RANGE' } ], ProvisionedThroughput: { ReadCapacityUnits: 42, WriteCapacityUnits: 1337 }, GlobalSecondaryIndexes: [], + LocalSecondaryIndexes: [], AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, { AttributeName: 'sortKey', AttributeType: 'N' } ], @@ -1491,6 +1741,7 @@ export = { { AttributeName: 'sortKey', KeyType: 'RANGE' } ], ProvisionedThroughput: { ReadCapacityUnits: 42, WriteCapacityUnits: 1337 }, GlobalSecondaryIndexes: [], + LocalSecondaryIndexes: [], AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, { AttributeName: 'sortKey', AttributeType: 'N' } ], @@ -1571,6 +1822,7 @@ export = { { AttributeName: 'sortKey', KeyType: 'RANGE' } ], ProvisionedThroughput: { ReadCapacityUnits: 42, WriteCapacityUnits: 1337 }, GlobalSecondaryIndexes: [], + LocalSecondaryIndexes: [], AttributeDefinitions: [ { AttributeName: 'hashKey', AttributeType: 'S' }, { AttributeName: 'sortKey', AttributeType: 'N' } ] } },