packages/aws-cdk-lib/aws-dynamodb/lib/table-v2.ts (543 lines of code) (raw):

import { Construct } from 'constructs'; import { Billing } from './billing'; import { Capacity } from './capacity'; import { CfnGlobalTable } from './dynamodb.generated'; import { TableEncryptionV2 } from './encryption'; import { Attribute, BillingMode, LocalSecondaryIndexProps, ProjectionType, SecondaryIndexProps, StreamViewType, PointInTimeRecoverySpecification, TableClass, WarmThroughput, } from './shared'; import { ITableV2, TableBaseV2 } from './table-v2-base'; import { PolicyDocument } from '../../aws-iam'; import { IStream } from '../../aws-kinesis'; import { IKey, Key } from '../../aws-kms'; import { ArnFormat, CfnTag, FeatureFlags, Lazy, PhysicalName, RemovalPolicy, Stack, TagManager, TagType, Token, } from '../../core'; import { ValidationError } from '../../core/lib/errors'; import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource'; import * as cxapi from '../../cx-api'; const HASH_KEY_TYPE = 'HASH'; const RANGE_KEY_TYPE = 'RANGE'; const MAX_GSI_COUNT = 20; const MAX_LSI_COUNT = 5; const MAX_NON_KEY_ATTRIBUTES = 100; /** * Options used to configure global secondary indexes on a replica table. */ export interface ReplicaGlobalSecondaryIndexOptions { /** * Whether CloudWatch contributor insights is enabled for a specific global secondary * index on a replica table. * * @default - inherited from the primary table */ readonly contributorInsights?: boolean; /** * The read capacity for a specific global secondary index on a replica table. * * Note: This can only be configured if primary table billing is provisioned. * * @default - inherited from the primary table */ readonly readCapacity?: Capacity; /** * The maximum read request units for a specific global secondary index on a replica table. * * Note: This can only be configured if primary table billing is PAY_PER_REQUEST. * * @default - inherited from the primary table */ readonly maxReadRequestUnits?: number; } /** * Properties used to configure a global secondary index. */ export interface GlobalSecondaryIndexPropsV2 extends SecondaryIndexProps { /** * Partition key attribute definition. */ readonly partitionKey: Attribute; /** * Sort key attribute definition. * * @default - no sort key */ readonly sortKey?: Attribute; /** * The read capacity. * * Note: This can only be configured if the primary table billing is provisioned. * * @default - inherited from the primary table. */ readonly readCapacity?: Capacity; /** * The write capacity. * * Note: This can only be configured if the primary table billing is provisioned. * * @default - inherited from the primary table. */ readonly writeCapacity?: Capacity; /** * The maximum read request units. * * Note: This can only be configured if the primary table billing is PAY_PER_REQUEST. * * @default - inherited from the primary table. */ readonly maxReadRequestUnits?: number; /** * The maximum write request units. * * Note: This can only be configured if the primary table billing is PAY_PER_REQUEST. * * @default - inherited from the primary table. */ readonly maxWriteRequestUnits?: number; /** * The warm throughput configuration for the global secondary index. * * @default - no warm throughput is configured */ readonly warmThroughput?: WarmThroughput; } /** * Options used to configure a DynamoDB table. */ export interface TableOptionsV2 { /** * Whether CloudWatch contributor insights is enabled. * * @default false */ readonly contributorInsights?: boolean; /** * Whether deletion protection is enabled. * * @default false */ readonly deletionProtection?: boolean; /** * Whether point-in-time recovery is enabled. * @deprecated use `pointInTimeRecoverySpecification` instead * @default false - point in time recovery is not enabled. */ readonly pointInTimeRecovery?: boolean; /** * Whether point-in-time recovery is enabled * and recoveryPeriodInDays is set. * * @default - point in time recovery is not enabled. */ readonly pointInTimeRecoverySpecification?: PointInTimeRecoverySpecification; /** * The table class. * * @default TableClass.STANDARD */ readonly tableClass?: TableClass; /** * Kinesis Data Stream to capture item level changes. * * @default - no Kinesis Data Stream */ readonly kinesisStream?: IStream; /** * Tags to be applied to the primary table (default replica table). * * @default - no tags */ readonly tags?: CfnTag[]; /** * Resource policy to assign to DynamoDB Table. * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-globaltable-replicaspecification.html#cfn-dynamodb-globaltable-replicaspecification-resourcepolicy * @default - No resource policy statements are added to the created table. */ readonly resourcePolicy?: PolicyDocument; } /** * Properties used to configure a replica table. */ export interface ReplicaTableProps extends TableOptionsV2 { /** * The region that the replica table will be created in. */ readonly region: string; /** * The read capacity. * * Note: This can only be configured if the primary table billing is provisioned. * * @default - inherited from the primary table */ readonly readCapacity?: Capacity; /** * The maximum read request units. * * Note: This can only be configured if the primary table billing is PAY_PER_REQUEST. * * @default - inherited from the primary table */ readonly maxReadRequestUnits?: number; /** * Options used to configure global secondary index properties. * * @default - inherited from the primary table */ readonly globalSecondaryIndexOptions?: { [indexName: string]: ReplicaGlobalSecondaryIndexOptions }; } /** * Properties used to configure a DynamoDB table. */ export interface TablePropsV2 extends TableOptionsV2 { /** * Partition key attribute definition. */ readonly partitionKey: Attribute; /** * Sort key attribute definition. * * @default - no sort key */ readonly sortKey?: Attribute; /** * The name of the table. * * @default - generated by CloudFormation */ readonly tableName?: string; /** * The name of the TTL attribute. * * @default - TTL is disabled */ readonly timeToLiveAttribute?: string; /** * When an item in the table is modified, StreamViewType determines what information is * written to the stream. * * @default - streams are disabled if replicas are not configured and this property is * not specified. If this property is not specified when replicas are configured, then * NEW_AND_OLD_IMAGES will be the StreamViewType for all replicas */ readonly dynamoStream?: StreamViewType; /** * The removal policy applied to the table. * * @default RemovalPolicy.RETAIN */ readonly removalPolicy?: RemovalPolicy; /** * The billing mode and capacity settings to apply to the table. * * @default Billing.onDemand() */ readonly billing?: Billing; /** * Replica tables to deploy with the primary table. * * Note: Adding replica tables allows you to use your table as a global table. You * cannot specify a replica table in the region that the primary table will be deployed * to. Replica tables will only be supported if the stack deployment region is defined. * * @default - no replica tables */ readonly replicas?: ReplicaTableProps[]; /** * Global secondary indexes. * * Note: You can provide a maximum of 20 global secondary indexes. * * @default - no global secondary indexes */ readonly globalSecondaryIndexes?: GlobalSecondaryIndexPropsV2[]; /** * Local secondary indexes. * * Note: You can only provide a maximum of 5 local secondary indexes. * * @default - no local secondary indexes */ readonly localSecondaryIndexes?: LocalSecondaryIndexProps[]; /** * The server-side encryption. * * @default TableEncryptionV2.dynamoOwnedKey() */ readonly encryption?: TableEncryptionV2; /** * The warm throughput configuration for the table. * * @default - no warm throughput is configured */ readonly warmThroughput?: WarmThroughput; } /** * Attributes of a DynamoDB table. */ export interface TableAttributesV2 { /** * The ARN of the table. * * Note: You must specify this or the `tableName`. * * @default - table arn generated using `tableName` and region of stack */ readonly tableArn?: string; /** * The name of the table. * * Note: You must specify this or the `tableArn`. * * @default - table name retrieved from provided `tableArn` */ readonly tableName?: string; /** * The ID of the table. * * @default - no table id */ readonly tableId?: string; /** * The stream ARN of the table. * * @default - no table stream ARN */ readonly tableStreamArn?: string; /** * KMS encryption key for the table. * * @default - no KMS encryption key */ readonly encryptionKey?: IKey; /** * The name of the global indexes set for the table. * * Note: You must set either this property or `localIndexes` if you want permissions * to be granted for indexes as well as the table itself. * * @default - no global indexes */ readonly globalIndexes?: string[]; /** * The name of the local indexes set for the table. * * Note: You must set either this property or `globalIndexes` if you want permissions * to be granted for indexes as well as the table itself. * * @default - no local indexes */ readonly localIndexes?: string[]; /** * Whether or not to grant permissions for all indexes of the table. * * Note: If false, permissions will only be granted to indexes when `globalIndexes` * or `localIndexes` is specified. * * @default false */ readonly grantIndexPermissions?: boolean; } /** * A DynamoDB Table. */ export class TableV2 extends TableBaseV2 { /** * Creates a Table construct that represents an external table via table name. * * @param scope the parent creating construct (usually `this`) * @param id the construct's name * @param tableName the table's name */ public static fromTableName(scope: Construct, id: string, tableName: string): ITableV2 { return TableV2.fromTableAttributes(scope, id, { tableName }); } /** * Creates a Table construct that represents an external table via table ARN. * * @param scope the parent creating construct (usually `this`) * @param id the construct's name * @param tableArn the table's ARN */ public static fromTableArn(scope: Construct, id: string, tableArn: string): ITableV2 { return TableV2.fromTableAttributes(scope, id, { tableArn }); } /** * Creates a Table construct that represents an external table. * * @param scope the parent creating construct (usually `this`) * @param id the construct's name * @param attrs attributes of the table */ public static fromTableAttributes(scope: Construct, id: string, attrs: TableAttributesV2): ITableV2 { class Import extends TableBaseV2 { public readonly tableArn: string; public readonly tableName: string; public readonly tableId?: string; public readonly tableStreamArn?: string; public readonly encryptionKey?: IKey; public readonly resourcePolicy?: PolicyDocument; protected readonly region: string; protected readonly hasIndex = (attrs.grantIndexPermissions ?? false) || (attrs.globalIndexes ?? []).length > 0 || (attrs.localIndexes ?? []).length > 0; public constructor(tableArn: string, tableName: string, tableId?: string, tableStreamArn?: string, resourcePolicy?: PolicyDocument) { super(scope, id, { environmentFromArn: tableArn }); const resourceRegion = stack.splitArn(tableArn, ArnFormat.SLASH_RESOURCE_NAME).region; if (!resourceRegion) { throw new ValidationError('Table ARN must be of the form: arn:<partition>:dynamodb:<region>:<account>:table/<table-name>', this); } this.region = resourceRegion; this.tableArn = tableArn; this.tableName = tableName; this.tableId = tableId; this.tableStreamArn = tableStreamArn; this.encryptionKey = attrs.encryptionKey; this.resourcePolicy = resourcePolicy; } } let tableName: string; let tableArn: string; const stack = Stack.of(scope); if (!attrs.tableArn) { if (!attrs.tableName) { throw new ValidationError('At least one of `tableArn` or `tableName` must be provided', scope); } tableName = attrs.tableName; tableArn = stack.formatArn({ service: 'dynamodb', resource: 'table', resourceName: tableName, }); } else { if (attrs.tableName) { throw new ValidationError('Only one of `tableArn` or `tableName` can be provided, but not both', scope); } tableArn = attrs.tableArn; const resourceName = stack.splitArn(tableArn, ArnFormat.SLASH_RESOURCE_NAME).resourceName; if (!resourceName) { throw new ValidationError('Table ARN must be of the form: arn:<partition>:dynamodb:<region>:<account>:table/<table-name>', scope); } tableName = resourceName; } return new Import(tableArn, tableName, attrs.tableId, attrs.tableStreamArn); } /** * @attribute */ public readonly tableArn: string; /** * @attribute */ public readonly tableName: string; /** * @attribute */ public readonly tableStreamArn?: string; /** * @attribute */ public readonly tableId?: string; public readonly encryptionKey?: IKey; /** * @attribute */ public resourcePolicy?: PolicyDocument; protected readonly region: string; protected readonly tags: TagManager; private readonly billingMode: string; private readonly partitionKey: Attribute; private readonly hasSortKey: boolean; private readonly tableOptions: TableOptionsV2; private readonly encryption?: TableEncryptionV2; private readonly keySchema: CfnGlobalTable.KeySchemaProperty[] = []; private readonly attributeDefinitions: CfnGlobalTable.AttributeDefinitionProperty[] = []; private readonly nonKeyAttributes = new Set<string>(); private readonly readProvisioning?: CfnGlobalTable.ReadProvisionedThroughputSettingsProperty; private readonly writeProvisioning?: CfnGlobalTable.WriteProvisionedThroughputSettingsProperty; private readonly maxReadRequestUnits?: number; private readonly maxWriteRequestUnits?: number; private readonly replicaTables = new Map<string, ReplicaTableProps>(); private readonly replicaKeys: { [region: string]: IKey } = {}; private readonly replicaTableArns: string[] = []; private readonly replicaStreamArns: string[] = []; private readonly globalSecondaryIndexes = new Map<string, CfnGlobalTable.GlobalSecondaryIndexProperty>(); private readonly localSecondaryIndexes = new Map<string, CfnGlobalTable.LocalSecondaryIndexProperty>(); private readonly globalSecondaryIndexReadCapacitys = new Map<string, Capacity>(); private readonly globalSecondaryIndexMaxReadUnits = new Map<string, number>(); public constructor(scope: Construct, id: string, props: TablePropsV2) { super(scope, id, { physicalName: props.tableName ?? PhysicalName.GENERATE_IF_NEEDED }); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); this.tableOptions = props; this.partitionKey = props.partitionKey; this.hasSortKey = props.sortKey !== undefined; this.region = this.stack.region; this.tags = new TagManager(TagType.STANDARD, CfnGlobalTable.CFN_RESOURCE_TYPE_NAME); this.encryption = props.encryption; this.encryptionKey = this.encryption?.tableKey; this.configureReplicaKeys(this.encryption?.replicaKeyArns); this.addKey(props.partitionKey, HASH_KEY_TYPE); if (props.sortKey) { this.addKey(props.sortKey, RANGE_KEY_TYPE); } this.validatePitr(props); if (props.billing?.mode === BillingMode.PAY_PER_REQUEST || props.billing?.mode === undefined) { this.maxReadRequestUnits = props.billing?._renderReadCapacity(); this.maxWriteRequestUnits = props.billing?._renderWriteCapacity(); this.billingMode = BillingMode.PAY_PER_REQUEST; } else { this.readProvisioning = props.billing?._renderReadCapacity(); this.writeProvisioning = props.billing?._renderWriteCapacity(); this.billingMode = props.billing.mode; } props.globalSecondaryIndexes?.forEach(gsi => this.addGlobalSecondaryIndex(gsi)); props.localSecondaryIndexes?.forEach(lsi => this.addLocalSecondaryIndex(lsi)); const resource = new CfnGlobalTable(this, 'Resource', { tableName: this.physicalName, keySchema: this.keySchema, attributeDefinitions: Lazy.any({ produce: () => this.attributeDefinitions }), replicas: Lazy.any({ produce: () => this.renderReplicaTables() }), globalSecondaryIndexes: Lazy.any({ produce: () => this.renderGlobalIndexes() }, { omitEmptyArray: true }), localSecondaryIndexes: Lazy.any({ produce: () => this.renderLocalIndexes() }, { omitEmptyArray: true }), billingMode: this.billingMode, writeProvisionedThroughputSettings: this.writeProvisioning, writeOnDemandThroughputSettings: this.maxWriteRequestUnits ? { maxWriteRequestUnits: this.maxWriteRequestUnits } : undefined, streamSpecification: Lazy.any( { produce: () => props.dynamoStream ? { streamViewType: props.dynamoStream } : this.renderStreamSpecification() }, ), sseSpecification: this.encryption?._renderSseSpecification(), timeToLiveSpecification: props.timeToLiveAttribute ? { attributeName: props.timeToLiveAttribute, enabled: true } : undefined, warmThroughput: props.warmThroughput ?? undefined, }); resource.applyRemovalPolicy(props.removalPolicy); this.tableArn = this.getResourceArnAttribute(resource.attrArn, { service: 'dynamodb', resource: 'table', resourceName: this.physicalName, }); this.tableName = this.getResourceNameAttribute(resource.ref); this.tableId = resource.attrTableId; this.tableStreamArn = resource.attrStreamArn; props.replicas?.forEach(replica => this.addReplica(replica)); if (props.tableName) { this.node.addMetadata('aws:cdk:hasPhysicalName', this.tableName); } } /** * Add a replica table. * * Note: Adding a replica table will allow you to use your table as a global table. * * @param props the properties of the replica table to add */ @MethodMetadata() public addReplica(props: ReplicaTableProps) { this.validateReplica(props); const replicaArn = this.stack.formatArn({ region: props.region, resource: 'table', service: 'dynamodb', resourceName: this.tableName, }); this.replicaTableArns.push(replicaArn); const replicaStreamArn = `${replicaArn}/stream/*`; this.replicaStreamArns.push(replicaStreamArn); this.replicaTables.set(props.region, props); } /** * Add a global secondary index to the table. * * Note: Global secondary indexes will be inherited by all replica tables. * * @param props the properties of the global secondary index */ @MethodMetadata() public addGlobalSecondaryIndex(props: GlobalSecondaryIndexPropsV2) { this.validateGlobalSecondaryIndex(props); const globalSecondaryIndex = this.configureGlobalSecondaryIndex(props); this.globalSecondaryIndexes.set(props.indexName, globalSecondaryIndex); } /** * Add a local secondary index to the table. * * Note: Local secondary indexes will be inherited by all replica tables. * * @param props the properties of the local secondary index */ @MethodMetadata() public addLocalSecondaryIndex(props: LocalSecondaryIndexProps) { this.validateLocalSecondaryIndex(props); const localSecondaryIndex = this.configureLocalSecondaryIndex(props); this.localSecondaryIndexes.set(props.indexName, localSecondaryIndex); } /** * Retrieve a replica table. * * Note: Replica tables are not supported in a region agnostic stack. * * @param region the region of the replica table */ @MethodMetadata() public replica(region: string): ITableV2 { if (Token.isUnresolved(this.stack.region)) { throw new ValidationError('Replica tables are not supported in a region agnostic stack', this); } if (Token.isUnresolved(region)) { throw new ValidationError('Provided `region` cannot be a token', this); } if (region === this.stack.region) { return this; } if (!this.replicaTables.has(region)) { throw new ValidationError(`No replica table exists in region ${region}`, this); } const replicaTableArn = this.replicaTableArns.find(arn => arn.includes(region)); const replicaStreamArn = this.replicaStreamArns.find(arn => arn.includes(region)); return TableV2.fromTableAttributes(this, `ReplicaTable${region}`, { tableArn: replicaTableArn, encryptionKey: this.replicaKeys[region], grantIndexPermissions: this.hasIndex, tableStreamArn: replicaStreamArn, }); } private configureReplicaTable(props: ReplicaTableProps): CfnGlobalTable.ReplicaSpecificationProperty { const contributorInsights = props.contributorInsights ?? this.tableOptions.contributorInsights; // Determine if Point-In-Time Recovery (PITR) is enabled based on the provided property or table options (deprecated options). const pointInTimeRecovery = props.pointInTimeRecovery ?? this.tableOptions.pointInTimeRecovery; /* Construct the PointInTimeRecoverySpecification object to configure PITR settings. * 1. Explicitly provided specification via props.pointInTimeRecoverySpecification. * 2. Fallback to default specification from tableOptions.pointInTimeRecoverySpecification. * 3. Derive the specification based on pointInTimeRecovery if it's defined. */ const pointInTimeRecoverySpecification: PointInTimeRecoverySpecification | undefined = props.pointInTimeRecoverySpecification ?? this.tableOptions.pointInTimeRecoverySpecification ?? (pointInTimeRecovery !== undefined ? { pointInTimeRecoveryEnabled: pointInTimeRecovery } : undefined); /* * Feature flag set as the following may be a breaking change. * @see https://github.com/aws/aws-cdk/pull/31097 * @see https://github.com/aws/aws-cdk/blob/main/packages/%40aws-cdk/cx-api/FEATURE_FLAGS.md */ const resourcePolicy = FeatureFlags.of(this).isEnabled(cxapi.DYNAMODB_TABLEV2_RESOURCE_POLICY_PER_REPLICA) ? (props.region === this.region ? this.tableOptions.resourcePolicy : props.resourcePolicy) || undefined : props.resourcePolicy ?? this.tableOptions.resourcePolicy; const propTags: Record<string, string> = (props.tags ?? []).reduce((p, item) => ({ ...p, [item.key]: item.value }), {}, ); const tags: CfnTag[] = Object.entries({ ...this.tags.tagValues(), ...propTags, }).map(([k, v]) => ({ key: k, value: v })); return { region: props.region, globalSecondaryIndexes: this.configureReplicaGlobalSecondaryIndexes(props.globalSecondaryIndexOptions), deletionProtectionEnabled: props.deletionProtection ?? this.tableOptions.deletionProtection, tableClass: props.tableClass ?? this.tableOptions.tableClass, sseSpecification: this.encryption?._renderReplicaSseSpecification(this, props.region), kinesisStreamSpecification: props.kinesisStream ? { streamArn: props.kinesisStream.streamArn } : undefined, contributorInsightsSpecification: contributorInsights !== undefined ? { enabled: contributorInsights } : undefined, pointInTimeRecoverySpecification: pointInTimeRecoverySpecification, readProvisionedThroughputSettings: props.readCapacity ? props.readCapacity._renderReadCapacity() : this.readProvisioning, tags: tags.length === 0 ? undefined : tags, readOnDemandThroughputSettings: props.maxReadRequestUnits ? { maxReadRequestUnits: props.maxReadRequestUnits } : this.maxReadRequestUnits ? { maxReadRequestUnits: this.maxReadRequestUnits } : undefined, resourcePolicy: resourcePolicy ? { policyDocument: resourcePolicy } : undefined, }; } private configureGlobalSecondaryIndex(props: GlobalSecondaryIndexPropsV2): CfnGlobalTable.GlobalSecondaryIndexProperty { const keySchema = this.configureIndexKeySchema(props.partitionKey, props.sortKey); const projection = this.configureIndexProjection(props); props.readCapacity && this.globalSecondaryIndexReadCapacitys.set(props.indexName, props.readCapacity); const writeProvisionedThroughputSettings = props.writeCapacity ? props.writeCapacity._renderWriteCapacity() : this.writeProvisioning; props.maxReadRequestUnits && this.globalSecondaryIndexMaxReadUnits.set(props.indexName, props.maxReadRequestUnits); const warmThroughput = props.warmThroughput ?? undefined; const writeOnDemandThroughputSettings: CfnGlobalTable.WriteOnDemandThroughputSettingsProperty | undefined = props.maxWriteRequestUnits ? { maxWriteRequestUnits: props.maxWriteRequestUnits } : undefined; return { indexName: props.indexName, keySchema, projection, writeProvisionedThroughputSettings, writeOnDemandThroughputSettings, warmThroughput, }; } private configureLocalSecondaryIndex(props: LocalSecondaryIndexProps): CfnGlobalTable.LocalSecondaryIndexProperty { const keySchema = this.configureIndexKeySchema(this.partitionKey, props.sortKey); const projection = this.configureIndexProjection(props); return { indexName: props.indexName, keySchema, projection, }; } private configureReplicaGlobalSecondaryIndexes(options: { [indexName: string]: ReplicaGlobalSecondaryIndexOptions } = {}) { this.validateReplicaIndexOptions(options); const replicaGlobalSecondaryIndexes: CfnGlobalTable.ReplicaGlobalSecondaryIndexSpecificationProperty[] = []; const indexNamesFromOptions = Object.keys(options); for (const gsi of this.globalSecondaryIndexes.values()) { const indexName = gsi.indexName; let contributorInsights = this.tableOptions.contributorInsights; let readCapacity = this.globalSecondaryIndexReadCapacitys.get(indexName); let maxReadRequestUnits = this.globalSecondaryIndexMaxReadUnits.get(indexName); if (indexNamesFromOptions.includes(indexName)) { const indexOptions = options[indexName]; contributorInsights = indexOptions.contributorInsights; readCapacity = indexOptions.readCapacity; maxReadRequestUnits = indexOptions.maxReadRequestUnits; } const readProvisionedThroughputSettings = readCapacity?._renderReadCapacity() ?? this.readProvisioning; const readOnDemandThroughputSettings: CfnGlobalTable.ReadOnDemandThroughputSettingsProperty | undefined = maxReadRequestUnits ? { maxReadRequestUnits: maxReadRequestUnits } : undefined; replicaGlobalSecondaryIndexes.push({ indexName, readProvisionedThroughputSettings, readOnDemandThroughputSettings, contributorInsightsSpecification: contributorInsights !== undefined ? { enabled: contributorInsights } : undefined, }); } return replicaGlobalSecondaryIndexes.length > 0 ? replicaGlobalSecondaryIndexes : undefined; } private configureIndexKeySchema(partitionKey: Attribute, sortKey?: Attribute) { this.addAttributeDefinition(partitionKey); const indexKeySchema: CfnGlobalTable.KeySchemaProperty[] = [ { attributeName: partitionKey.name, keyType: HASH_KEY_TYPE }, ]; if (sortKey) { this.addAttributeDefinition(sortKey); indexKeySchema.push({ attributeName: sortKey.name, keyType: RANGE_KEY_TYPE }); } return indexKeySchema; } private configureIndexProjection(props: SecondaryIndexProps): CfnGlobalTable.ProjectionProperty { this.validateIndexProjection(props); props.nonKeyAttributes?.forEach(attr => this.nonKeyAttributes.add(attr)); if (this.nonKeyAttributes.size > MAX_NON_KEY_ATTRIBUTES) { throw new ValidationError(`The maximum number of 'nonKeyAttributes' across all secondary indexes is ${MAX_NON_KEY_ATTRIBUTES}`, this); } return { projectionType: props.projectionType ?? ProjectionType.ALL, nonKeyAttributes: props.nonKeyAttributes ?? undefined, }; } private configureReplicaKeys(replicaKeyArns: { [region: string]: string } = {}) { for (const [region, keyArn] of Object.entries(replicaKeyArns)) { this.replicaKeys[region] = Key.fromKeyArn(this, `ReplicaKey${region}`, keyArn); } } private renderReplicaTables() { const replicaTables: CfnGlobalTable.ReplicaSpecificationProperty[] = []; for (const replicaTable of this.replicaTables.values()) { replicaTables.push(this.configureReplicaTable(replicaTable)); } replicaTables.push(this.configureReplicaTable({ region: this.stack.region, kinesisStream: this.tableOptions.kinesisStream, tags: this.tableOptions.tags, })); return replicaTables; } private renderGlobalIndexes() { const globalSecondaryIndexes: CfnGlobalTable.GlobalSecondaryIndexProperty[] = []; for (const globalSecondaryIndex of this.globalSecondaryIndexes.values()) { globalSecondaryIndexes.push(globalSecondaryIndex); } return globalSecondaryIndexes; } private renderLocalIndexes() { const localSecondaryIndexes: CfnGlobalTable.LocalSecondaryIndexProperty[] = []; for (const localSecondaryIndex of this.localSecondaryIndexes.values()) { localSecondaryIndexes.push(localSecondaryIndex); } return localSecondaryIndexes; } private renderStreamSpecification(): CfnGlobalTable.StreamSpecificationProperty | undefined { return this.replicaTables.size > 0 ? { streamViewType: StreamViewType.NEW_AND_OLD_IMAGES } : undefined; } private addKey(key: Attribute, keyType: string) { this.addAttributeDefinition(key); this.keySchema.push({ attributeName: key.name, keyType }); } private addAttributeDefinition(attribute: Attribute) { const { name, type } = attribute; const existingAttributeDef = this.attributeDefinitions.find(def => def.attributeName === name); if (existingAttributeDef && existingAttributeDef.attributeType !== type) { throw new ValidationError(`Unable to specify ${name} as ${type} because it was already defined as ${existingAttributeDef.attributeType}`, this); } if (!existingAttributeDef) { this.attributeDefinitions.push({ attributeName: name, attributeType: type }); } } protected get hasIndex() { return this.globalSecondaryIndexes.size + this.localSecondaryIndexes.size > 0; } private validateIndexName(indexName: string) { if (this.globalSecondaryIndexes.has(indexName) || this.localSecondaryIndexes.has(indexName)) { throw new ValidationError(`Duplicate secondary index name, ${indexName}, is not allowed`, this); } } private validateIndexProjection(props: SecondaryIndexProps) { if (props.projectionType === ProjectionType.INCLUDE && !props.nonKeyAttributes) { throw new ValidationError(`Non-key attributes should be specified when using ${ProjectionType.INCLUDE} projection type`, this); } if (props.projectionType !== ProjectionType.INCLUDE && props.nonKeyAttributes) { throw new ValidationError(`Non-key attributes should not be specified when not using ${ProjectionType.INCLUDE} projection type`, this); } } private validateReplicaIndexOptions(options: { [indexName: string]: ReplicaGlobalSecondaryIndexOptions }) { for (const indexName of Object.keys(options)) { if (!this.globalSecondaryIndexes.has(indexName)) { throw new ValidationError(`Cannot configure replica global secondary index, ${indexName}, because it is not defined on the primary table`, this); } const replicaGsiOptions = options[indexName]; if (this.billingMode === BillingMode.PAY_PER_REQUEST && replicaGsiOptions.readCapacity) { throw new ValidationError(`Cannot configure 'readCapacity' for replica global secondary index, ${indexName}, because billing mode is ${BillingMode.PAY_PER_REQUEST}`, this); } } } private validateReplica(props: ReplicaTableProps) { const stackRegion = this.stack.region; if (Token.isUnresolved(stackRegion)) { throw new ValidationError('Replica tables are not supported in a region agnostic stack', this); } if (Token.isUnresolved(props.region)) { throw new ValidationError('Replica table region must not be a token', this); } if (props.region === this.stack.region) { throw new ValidationError(`You cannot add a replica table in the same region as the primary table - the primary table region is ${this.region}`, this); } if (this.replicaTables.has(props.region)) { throw new ValidationError(`Duplicate replica table region, ${props.region}, is not allowed`, this); } if (this.billingMode === BillingMode.PAY_PER_REQUEST && props.readCapacity) { throw new ValidationError(`You cannot provide 'readCapacity' on a replica table when the billing mode is ${BillingMode.PAY_PER_REQUEST}`, this); } } private validateGlobalSecondaryIndex(props: GlobalSecondaryIndexPropsV2) { this.validateIndexName(props.indexName); if (this.globalSecondaryIndexes.size === MAX_GSI_COUNT) { throw new ValidationError(`You may not provide more than ${MAX_GSI_COUNT} global secondary indexes`, this); } if (this.billingMode === BillingMode.PAY_PER_REQUEST && (props.readCapacity || props.writeCapacity)) { throw new ValidationError(`You cannot configure 'readCapacity' or 'writeCapacity' on a global secondary index when the billing mode is ${BillingMode.PAY_PER_REQUEST}`, this); } } private validateLocalSecondaryIndex(props: LocalSecondaryIndexProps) { this.validateIndexName(props.indexName); if (!this.hasSortKey) { throw new ValidationError('The table must have a sort key in order to add a local secondary index', this); } if (this.localSecondaryIndexes.size === MAX_LSI_COUNT) { throw new ValidationError(`You may not provide more than ${MAX_LSI_COUNT} local secondary indexes`, this); } } private validatePitr(props: TablePropsV2) { const recoveryPeriodInDays = props.pointInTimeRecoverySpecification?.recoveryPeriodInDays; if (props.pointInTimeRecovery !== undefined && props.pointInTimeRecoverySpecification !== undefined) { throw new ValidationError('`pointInTimeRecoverySpecification` and `pointInTimeRecovery` are set. Use `pointInTimeRecoverySpecification` only.', this); } if (!props.pointInTimeRecoverySpecification?.pointInTimeRecoveryEnabled && recoveryPeriodInDays) { throw new ValidationError('Cannot set `recoveryPeriodInDays` while `pointInTimeRecoveryEnabled` is set to false.', this); } if (recoveryPeriodInDays !== undefined && (recoveryPeriodInDays < 1 || recoveryPeriodInDays > 35 )) { throw new ValidationError('`recoveryPeriodInDays` must be a value between `1` and `35`.', this); } } }