@aws-cdk/aws-dynamodb
Version:
The CDK Construct Library for AWS::DynamoDB
1,099 lines (1,098 loc) • 181 kB
JavaScript
"use strict";
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.TableClass = exports.StreamViewType = exports.ProjectionType = exports.BillingMode = exports.AttributeType = exports.Table = exports.TableEncryption = exports.Operation = void 0;
const jsiiDeprecationWarnings = require("../.warnings.jsii.js");
const JSII_RTTI_SYMBOL_1 = Symbol.for("jsii.rtti");
const appscaling = require("@aws-cdk/aws-applicationautoscaling");
const cloudwatch = require("@aws-cdk/aws-cloudwatch");
const iam = require("@aws-cdk/aws-iam");
const kms = require("@aws-cdk/aws-kms");
const core_1 = require("@aws-cdk/core");
const dynamodb_canned_metrics_generated_1 = require("./dynamodb-canned-metrics.generated");
const dynamodb_generated_1 = require("./dynamodb.generated");
const perms = require("./perms");
const replica_provider_1 = require("./replica-provider");
const scalable_table_attribute_1 = require("./scalable-table-attribute");
// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
// eslint-disable-next-line no-duplicate-imports, import/order
const core_2 = require("@aws-cdk/core");
const HASH_KEY_TYPE = 'HASH';
const RANGE_KEY_TYPE = 'RANGE';
// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-secondary-indexes
const MAX_LOCAL_SECONDARY_INDEX_COUNT = 5;
/**
* Supported DynamoDB table operations.
*/
var Operation;
(function (Operation) {
/** GetItem */
Operation["GET_ITEM"] = "GetItem";
/** BatchGetItem */
Operation["BATCH_GET_ITEM"] = "BatchGetItem";
/** Scan */
Operation["SCAN"] = "Scan";
/** Query */
Operation["QUERY"] = "Query";
/** GetRecords */
Operation["GET_RECORDS"] = "GetRecords";
/** PutItem */
Operation["PUT_ITEM"] = "PutItem";
/** DeleteItem */
Operation["DELETE_ITEM"] = "DeleteItem";
/** UpdateItem */
Operation["UPDATE_ITEM"] = "UpdateItem";
/** BatchWriteItem */
Operation["BATCH_WRITE_ITEM"] = "BatchWriteItem";
/** TransactWriteItems */
Operation["TRANSACT_WRITE_ITEMS"] = "TransactWriteItems";
/** TransactGetItems */
Operation["TRANSACT_GET_ITEMS"] = "TransactGetItems";
/** ExecuteTransaction */
Operation["EXECUTE_TRANSACTION"] = "ExecuteTransaction";
/** BatchExecuteStatement */
Operation["BATCH_EXECUTE_STATEMENT"] = "BatchExecuteStatement";
/** ExecuteStatement */
Operation["EXECUTE_STATEMENT"] = "ExecuteStatement";
})(Operation = exports.Operation || (exports.Operation = {}));
/**
* What kind of server-side encryption to apply to this table.
*/
var TableEncryption;
(function (TableEncryption) {
/**
* Server-side KMS encryption with a master key owned by AWS.
*/
TableEncryption["DEFAULT"] = "AWS_OWNED";
/**
* Server-side KMS encryption with a customer master key managed by customer.
* If `encryptionKey` is specified, this key will be used, otherwise, one will be defined.
*
* > **NOTE**: if `encryptionKey` is not specified and the `Table` construct creates
* > a KMS key for you, the key will be created with default permissions. If you are using
* > CDKv2, these permissions will be sufficient to enable the key for use with DynamoDB tables.
* > If you are using CDKv1, make sure the feature flag `@aws-cdk/aws-kms:defaultKeyPolicies`
* > is set to `true` in your `cdk.json`.
*/
TableEncryption["CUSTOMER_MANAGED"] = "CUSTOMER_MANAGED";
/**
* Server-side KMS encryption with a master key managed by AWS.
*/
TableEncryption["AWS_MANAGED"] = "AWS_MANAGED";
})(TableEncryption = exports.TableEncryption || (exports.TableEncryption = {}));
class TableBase extends core_1.Resource {
constructor() {
super(...arguments);
this.regionalArns = new Array();
}
/**
* Adds an IAM policy statement associated with this table to an IAM
* principal's policy.
*
* If `encryptionKey` is present, appropriate grants to the key needs to be added
* separately using the `table.encryptionKey.grant*` methods.
*
* @param grantee The principal (no-op if undefined)
* @param actions The set of actions to allow (i.e. "dynamodb:PutItem", "dynamodb:GetItem", ...)
*/
grant(grantee, ...actions) {
return iam.Grant.addToPrincipal({
grantee,
actions,
resourceArns: [
this.tableArn,
core_1.Lazy.string({ produce: () => this.hasIndex ? `${this.tableArn}/index/*` : core_1.Aws.NO_VALUE }),
...this.regionalArns,
...this.regionalArns.map(arn => core_1.Lazy.string({
produce: () => this.hasIndex ? `${arn}/index/*` : core_1.Aws.NO_VALUE,
})),
],
scope: this,
});
}
/**
* Adds an IAM policy statement associated with this table's stream to an
* IAM principal's policy.
*
* If `encryptionKey` is present, appropriate grants to the key needs to be added
* separately using the `table.encryptionKey.grant*` methods.
*
* @param grantee The principal (no-op if undefined)
* @param actions The set of actions to allow (i.e. "dynamodb:DescribeStream", "dynamodb:GetRecords", ...)
*/
grantStream(grantee, ...actions) {
if (!this.tableStreamArn) {
throw new Error(`DynamoDB Streams must be enabled on the table ${this.node.path}`);
}
return iam.Grant.addToPrincipal({
grantee,
actions,
resourceArns: [this.tableStreamArn],
scope: this,
});
}
/**
* Permits an IAM principal all data read operations from this table:
* BatchGetItem, GetRecords, GetShardIterator, Query, GetItem, Scan, DescribeTable.
*
* Appropriate grants will also be added to the customer-managed KMS key
* if one was configured.
*
* @param grantee The principal to grant access to
*/
grantReadData(grantee) {
const tableActions = perms.READ_DATA_ACTIONS.concat(perms.DESCRIBE_TABLE);
return this.combinedGrant(grantee, { keyActions: perms.KEY_READ_ACTIONS, tableActions });
}
/**
* Permits an IAM Principal to list streams attached to current dynamodb table.
*
* @param grantee The principal (no-op if undefined)
*/
grantTableListStreams(grantee) {
if (!this.tableStreamArn) {
throw new Error(`DynamoDB Streams must be enabled on the table ${this.node.path}`);
}
return iam.Grant.addToPrincipal({
grantee,
actions: ['dynamodb:ListStreams'],
resourceArns: ['*'],
});
}
/**
* Permits an IAM principal all stream data read operations for this
* table's stream:
* DescribeStream, GetRecords, GetShardIterator, ListStreams.
*
* Appropriate grants will also be added to the customer-managed KMS key
* if one was configured.
*
* @param grantee The principal to grant access to
*/
grantStreamRead(grantee) {
this.grantTableListStreams(grantee);
return this.combinedGrant(grantee, { keyActions: perms.KEY_READ_ACTIONS, streamActions: perms.READ_STREAM_DATA_ACTIONS });
}
/**
* Permits an IAM principal all data write operations to this table:
* BatchWriteItem, PutItem, UpdateItem, DeleteItem, DescribeTable.
*
* Appropriate grants will also be added to the customer-managed KMS key
* if one was configured.
*
* @param grantee The principal to grant access to
*/
grantWriteData(grantee) {
const tableActions = perms.WRITE_DATA_ACTIONS.concat(perms.DESCRIBE_TABLE);
const keyActions = perms.KEY_READ_ACTIONS.concat(perms.KEY_WRITE_ACTIONS);
return this.combinedGrant(grantee, { keyActions, tableActions });
}
/**
* Permits an IAM principal to all data read/write operations to this table.
* BatchGetItem, GetRecords, GetShardIterator, Query, GetItem, Scan,
* BatchWriteItem, PutItem, UpdateItem, DeleteItem, DescribeTable
*
* Appropriate grants will also be added to the customer-managed KMS key
* if one was configured.
*
* @param grantee The principal to grant access to
*/
grantReadWriteData(grantee) {
const tableActions = perms.READ_DATA_ACTIONS.concat(perms.WRITE_DATA_ACTIONS).concat(perms.DESCRIBE_TABLE);
const keyActions = perms.KEY_READ_ACTIONS.concat(perms.KEY_WRITE_ACTIONS);
return this.combinedGrant(grantee, { keyActions, tableActions });
}
/**
* Permits all DynamoDB operations ("dynamodb:*") to an IAM principal.
*
* Appropriate grants will also be added to the customer-managed KMS key
* if one was configured.
*
* @param grantee The principal to grant access to
*/
grantFullAccess(grantee) {
const keyActions = perms.KEY_READ_ACTIONS.concat(perms.KEY_WRITE_ACTIONS);
return this.combinedGrant(grantee, { keyActions, tableActions: ['dynamodb:*'] });
}
/**
* Return the given named metric for this Table
*
* By default, the metric will be calculated as a sum over a period of 5 minutes.
* You can customize this by using the `statistic` and `period` properties.
*/
metric(metricName, props) {
return new cloudwatch.Metric({
namespace: 'AWS/DynamoDB',
metricName,
dimensionsMap: {
TableName: this.tableName,
},
...props,
}).attachTo(this);
}
/**
* Metric for the consumed read capacity units this table
*
* By default, the metric will be calculated as a sum over a period of 5 minutes.
* You can customize this by using the `statistic` and `period` properties.
*/
metricConsumedReadCapacityUnits(props) {
return this.cannedMetric(dynamodb_canned_metrics_generated_1.DynamoDBMetrics.consumedReadCapacityUnitsSum, props);
}
/**
* Metric for the consumed write capacity units this table
*
* By default, the metric will be calculated as a sum over a period of 5 minutes.
* You can customize this by using the `statistic` and `period` properties.
*/
metricConsumedWriteCapacityUnits(props) {
return this.cannedMetric(dynamodb_canned_metrics_generated_1.DynamoDBMetrics.consumedWriteCapacityUnitsSum, props);
}
/**
* Metric for the system errors this table
*
* @deprecated use `metricSystemErrorsForOperations`.
*/
metricSystemErrors(props) {
if (!props?.dimensions?.Operation && !props?.dimensionsMap?.Operation) {
// 'Operation' must be passed because its an operational metric.
throw new Error("'Operation' dimension must be passed for the 'SystemErrors' metric.");
}
const dimensionsMap = {
TableName: this.tableName,
...props?.dimensions ?? {},
...props?.dimensionsMap ?? {},
};
return this.metric('SystemErrors', { statistic: 'sum', ...props, dimensionsMap });
}
/**
* Metric for the user errors. Note that this metric reports user errors across all
* the tables in the account and region the table resides in.
*
* By default, the metric will be calculated as a sum over a period of 5 minutes.
* You can customize this by using the `statistic` and `period` properties.
*/
metricUserErrors(props) {
if (props?.dimensions) {
throw new Error("'dimensions' is not supported for the 'UserErrors' metric");
}
// overriding 'dimensions' here because this metric is an account metric.
// see 'UserErrors' in https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/metrics-dimensions.html
return this.metric('UserErrors', { statistic: 'sum', ...props, dimensionsMap: {} });
}
/**
* Metric for the conditional check failed requests this table
*
* By default, the metric will be calculated as a sum over a period of 5 minutes.
* You can customize this by using the `statistic` and `period` properties.
*/
metricConditionalCheckFailedRequests(props) {
return this.metric('ConditionalCheckFailedRequests', { statistic: 'sum', ...props });
}
/**
* How many requests are throttled on this table
*
* Default: sum over 5 minutes
*
* @deprecated Do not use this function. It returns an invalid metric. Use `metricThrottledRequestsForOperation` instead.
*/
metricThrottledRequests(props) {
return this.metric('ThrottledRequests', { statistic: 'sum', ...props });
}
/**
* How many requests are throttled on this table, for the given operation
*
* Default: sum over 5 minutes
*/
metricThrottledRequestsForOperation(operation, props) {
return new cloudwatch.Metric({
...dynamodb_canned_metrics_generated_1.DynamoDBMetrics.throttledRequestsSum({ Operation: operation, TableName: this.tableName }),
...props,
}).attachTo(this);
}
/**
* Metric for the successful request latency this table.
*
* By default, the metric will be calculated as an average over a period of 5 minutes.
* You can customize this by using the `statistic` and `period` properties.
*/
metricSuccessfulRequestLatency(props) {
if (!props?.dimensions?.Operation && !props?.dimensionsMap?.Operation) {
throw new Error("'Operation' dimension must be passed for the 'SuccessfulRequestLatency' metric.");
}
const dimensionsMap = {
TableName: this.tableName,
Operation: props.dimensionsMap?.Operation ?? props.dimensions?.Operation,
};
return new cloudwatch.Metric({
...dynamodb_canned_metrics_generated_1.DynamoDBMetrics.successfulRequestLatencyAverage(dimensionsMap),
...props,
dimensionsMap,
}).attachTo(this);
}
/**
* Metric for the system errors this table.
*
* This will sum errors across all possible operations.
* Note that by default, each individual metric will be calculated as a sum over a period of 5 minutes.
* You can customize this by using the `statistic` and `period` properties.
*/
metricSystemErrorsForOperations(props) {
if (props?.dimensions?.Operation) {
throw new Error("The Operation dimension is not supported. Use the 'operations' property.");
}
const operations = props?.operations ?? Object.values(Operation);
const values = this.createMetricsForOperations('SystemErrors', operations, { statistic: 'sum', ...props });
const sum = new cloudwatch.MathExpression({
expression: `${Object.keys(values).join(' + ')}`,
usingMetrics: { ...values },
color: props?.color,
label: 'Sum of errors across all operations',
period: props?.period,
});
return sum;
}
/**
* Create a map of metrics that can be used in a math expression.
*
* Using the return value of this function as the `usingMetrics` property in `cloudwatch.MathExpression` allows you to
* use the keys of this map as metric names inside you expression.
*
* @param metricName The metric name.
* @param operations The list of operations to create metrics for.
* @param props Properties for the individual metrics.
* @param metricNameMapper Mapper function to allow controlling the individual metric name per operation.
*/
createMetricsForOperations(metricName, operations, props, metricNameMapper) {
const metrics = {};
const mapper = metricNameMapper ?? (op => op.toLowerCase());
if (props?.dimensions?.Operation) {
throw new Error('Invalid properties. Operation dimension is not supported when calculating operational metrics');
}
for (const operation of operations) {
const metric = this.metric(metricName, {
...props,
dimensionsMap: {
TableName: this.tableName,
Operation: operation,
...props?.dimensions,
},
});
const operationMetricName = mapper(operation);
const firstChar = operationMetricName.charAt(0);
if (firstChar === firstChar.toUpperCase()) {
// https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/using-metric-math.html#metric-math-syntax
throw new Error(`Mapper generated an illegal operation metric name: ${operationMetricName}. Must start with a lowercase letter`);
}
metrics[operationMetricName] = metric;
}
return metrics;
}
/**
* Adds an IAM policy statement associated with this table to an IAM
* principal's policy.
* @param grantee The principal (no-op if undefined)
* @param opts Options for keyActions, tableActions and streamActions
*/
combinedGrant(grantee, opts) {
if (opts.tableActions) {
const resources = [this.tableArn,
core_1.Lazy.string({ produce: () => this.hasIndex ? `${this.tableArn}/index/*` : core_1.Aws.NO_VALUE }),
...this.regionalArns,
...this.regionalArns.map(arn => core_1.Lazy.string({
produce: () => this.hasIndex ? `${arn}/index/*` : core_1.Aws.NO_VALUE,
}))];
const ret = iam.Grant.addToPrincipal({
grantee,
actions: opts.tableActions,
resourceArns: resources,
scope: this,
});
if (this.encryptionKey && opts.keyActions) {
this.encryptionKey.grant(grantee, ...opts.keyActions);
}
return ret;
}
if (opts.streamActions) {
if (!this.tableStreamArn) {
throw new Error(`DynamoDB Streams must be enabled on the table ${this.node.path}`);
}
const resources = [this.tableStreamArn];
const ret = iam.Grant.addToPrincipal({
grantee,
actions: opts.streamActions,
resourceArns: resources,
scope: this,
});
return ret;
}
throw new Error(`Unexpected 'action', ${opts.tableActions || opts.streamActions}`);
}
cannedMetric(fn, props) {
return new cloudwatch.Metric({
...fn({ TableName: this.tableName }),
...props,
}).attachTo(this);
}
}
/**
* Provides a DynamoDB table.
*/
class Table extends TableBase {
constructor(scope, id, props) {
super(scope, id, {
physicalName: props.tableName,
});
this.keySchema = new Array();
this.attributeDefinitions = new Array();
this.globalSecondaryIndexes = new Array();
this.localSecondaryIndexes = new Array();
this.secondaryIndexSchemas = new Map();
this.nonKeyAttributes = new Set();
this.tableScaling = {};
this.indexScaling = new Map();
this.globalReplicaCustomResources = new Array();
try {
jsiiDeprecationWarnings._aws_cdk_aws_dynamodb_TableProps(props);
}
catch (error) {
if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") {
Error.captureStackTrace(error, Table);
}
throw error;
}
const { sseSpecification, encryptionKey } = this.parseEncryption(props);
let streamSpecification;
if (props.replicationRegions) {
if (props.stream && props.stream !== StreamViewType.NEW_AND_OLD_IMAGES) {
throw new Error('`stream` must be set to `NEW_AND_OLD_IMAGES` when specifying `replicationRegions`');
}
streamSpecification = { streamViewType: StreamViewType.NEW_AND_OLD_IMAGES };
this.billingMode = props.billingMode ?? BillingMode.PAY_PER_REQUEST;
}
else {
this.billingMode = props.billingMode ?? BillingMode.PROVISIONED;
if (props.stream) {
streamSpecification = { streamViewType: props.stream };
}
}
this.validateProvisioning(props);
this.table = new dynamodb_generated_1.CfnTable(this, 'Resource', {
tableName: this.physicalName,
keySchema: this.keySchema,
attributeDefinitions: this.attributeDefinitions,
globalSecondaryIndexes: core_1.Lazy.any({ produce: () => this.globalSecondaryIndexes }, { omitEmptyArray: true }),
localSecondaryIndexes: core_1.Lazy.any({ produce: () => this.localSecondaryIndexes }, { omitEmptyArray: true }),
pointInTimeRecoverySpecification: props.pointInTimeRecovery != null ? { pointInTimeRecoveryEnabled: props.pointInTimeRecovery } : undefined,
billingMode: this.billingMode === BillingMode.PAY_PER_REQUEST ? this.billingMode : undefined,
provisionedThroughput: this.billingMode === BillingMode.PAY_PER_REQUEST ? undefined : {
readCapacityUnits: props.readCapacity || 5,
writeCapacityUnits: props.writeCapacity || 5,
},
sseSpecification,
streamSpecification,
tableClass: props.tableClass,
timeToLiveSpecification: props.timeToLiveAttribute ? { attributeName: props.timeToLiveAttribute, enabled: true } : undefined,
contributorInsightsSpecification: props.contributorInsightsEnabled !== undefined ? { enabled: props.contributorInsightsEnabled } : undefined,
kinesisStreamSpecification: props.kinesisStream ? { streamArn: props.kinesisStream.streamArn } : undefined,
});
this.table.applyRemovalPolicy(props.removalPolicy);
this.encryptionKey = encryptionKey;
this.tableArn = this.getResourceArnAttribute(this.table.attrArn, {
service: 'dynamodb',
resource: 'table',
resourceName: this.physicalName,
});
this.tableName = this.getResourceNameAttribute(this.table.ref);
if (props.tableName) {
this.node.addMetadata('aws:cdk:hasPhysicalName', this.tableName);
}
this.tableStreamArn = streamSpecification ? this.table.attrStreamArn : undefined;
this.scalingRole = this.makeScalingRole();
this.addKey(props.partitionKey, HASH_KEY_TYPE);
this.tablePartitionKey = props.partitionKey;
if (props.sortKey) {
this.addKey(props.sortKey, RANGE_KEY_TYPE);
this.tableSortKey = props.sortKey;
}
if (props.replicationRegions && props.replicationRegions.length > 0) {
this.createReplicaTables(props.replicationRegions, props.replicationTimeout, props.waitForReplicationToFinish);
}
}
/**
* Permits an IAM Principal to list all DynamoDB Streams.
* @deprecated Use {@link #grantTableListStreams} for more granular permission
* @param grantee The principal (no-op if undefined)
*/
static grantListStreams(grantee) {
try {
jsiiDeprecationWarnings.print("@aws-cdk/aws-dynamodb.Table#grantListStreams", "Use {@link #grantTableListStreams} for more granular permission");
}
catch (error) {
if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") {
Error.captureStackTrace(error, this.grantListStreams);
}
throw error;
}
return iam.Grant.addToPrincipal({
grantee,
actions: ['dynamodb:ListStreams'],
resourceArns: ['*'],
});
}
/**
* 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.
*/
static fromTableName(scope, id, tableName) {
return Table.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.
*/
static fromTableArn(scope, id, tableArn) {
return Table.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 A `TableAttributes` object.
*/
static fromTableAttributes(scope, id, attrs) {
try {
jsiiDeprecationWarnings._aws_cdk_aws_dynamodb_TableAttributes(attrs);
}
catch (error) {
if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") {
Error.captureStackTrace(error, this.fromTableAttributes);
}
throw error;
}
class Import extends TableBase {
constructor(_tableArn, tableName, tableStreamArn) {
super(scope, id);
this.hasIndex = (attrs.globalIndexes ?? []).length > 0 ||
(attrs.localIndexes ?? []).length > 0;
this.tableArn = _tableArn;
this.tableName = tableName;
this.tableStreamArn = tableStreamArn;
this.encryptionKey = attrs.encryptionKey;
}
}
let name;
let arn;
const stack = core_1.Stack.of(scope);
if (!attrs.tableName) {
if (!attrs.tableArn) {
throw new Error('One of tableName or tableArn is required!');
}
arn = attrs.tableArn;
const maybeTableName = stack.splitArn(attrs.tableArn, core_1.ArnFormat.SLASH_RESOURCE_NAME).resourceName;
if (!maybeTableName) {
throw new Error('ARN for DynamoDB table must be in the form: ...');
}
name = maybeTableName;
}
else {
if (attrs.tableArn) {
throw new Error('Only one of tableArn or tableName can be provided');
}
name = attrs.tableName;
arn = stack.formatArn({
service: 'dynamodb',
resource: 'table',
resourceName: attrs.tableName,
});
}
return new Import(arn, name, attrs.tableStreamArn);
}
/**
* Add a global secondary index of table.
*
* @param props the property of global secondary index
*/
addGlobalSecondaryIndex(props) {
try {
jsiiDeprecationWarnings._aws_cdk_aws_dynamodb_GlobalSecondaryIndexProps(props);
}
catch (error) {
if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") {
Error.captureStackTrace(error, this.addGlobalSecondaryIndex);
}
throw error;
}
this.validateProvisioning(props);
this.validateIndexName(props.indexName);
// build key schema and projection for index
const gsiKeySchema = this.buildIndexKeySchema(props.partitionKey, props.sortKey);
const gsiProjection = this.buildIndexProjection(props);
this.globalSecondaryIndexes.push({
indexName: props.indexName,
keySchema: gsiKeySchema,
projection: gsiProjection,
provisionedThroughput: this.billingMode === BillingMode.PAY_PER_REQUEST ? undefined : {
readCapacityUnits: props.readCapacity || 5,
writeCapacityUnits: props.writeCapacity || 5,
},
});
this.secondaryIndexSchemas.set(props.indexName, {
partitionKey: props.partitionKey,
sortKey: props.sortKey,
});
this.indexScaling.set(props.indexName, {});
}
/**
* Add a local secondary index of table.
*
* @param props the property of local secondary index
*/
addLocalSecondaryIndex(props) {
try {
jsiiDeprecationWarnings._aws_cdk_aws_dynamodb_LocalSecondaryIndexProps(props);
}
catch (error) {
if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") {
Error.captureStackTrace(error, this.addLocalSecondaryIndex);
}
throw error;
}
// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-secondary-indexes
if (this.localSecondaryIndexes.length >= MAX_LOCAL_SECONDARY_INDEX_COUNT) {
throw new RangeError(`a maximum number of local secondary index per table is ${MAX_LOCAL_SECONDARY_INDEX_COUNT}`);
}
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.localSecondaryIndexes.push({
indexName: props.indexName,
keySchema: lsiKeySchema,
projection: lsiProjection,
});
this.secondaryIndexSchemas.set(props.indexName, {
partitionKey: this.tablePartitionKey,
sortKey: props.sortKey,
});
}
/**
* Enable read capacity scaling for this table
*
* @returns An object to configure additional AutoScaling settings
*/
autoScaleReadCapacity(props) {
try {
jsiiDeprecationWarnings._aws_cdk_aws_dynamodb_EnableScalingProps(props);
}
catch (error) {
if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") {
Error.captureStackTrace(error, this.autoScaleReadCapacity);
}
throw error;
}
if (this.tableScaling.scalableReadAttribute) {
throw new Error('Read AutoScaling already enabled for this table');
}
if (this.billingMode === BillingMode.PAY_PER_REQUEST) {
throw new Error('AutoScaling is not available for tables with PAY_PER_REQUEST billing mode');
}
return this.tableScaling.scalableReadAttribute = new scalable_table_attribute_1.ScalableTableAttribute(this, 'ReadScaling', {
serviceNamespace: appscaling.ServiceNamespace.DYNAMODB,
resourceId: `table/${this.tableName}`,
dimension: 'dynamodb:table:ReadCapacityUnits',
role: this.scalingRole,
...props,
});
}
/**
* Enable write capacity scaling for this table
*
* @returns An object to configure additional AutoScaling settings for this attribute
*/
autoScaleWriteCapacity(props) {
try {
jsiiDeprecationWarnings._aws_cdk_aws_dynamodb_EnableScalingProps(props);
}
catch (error) {
if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") {
Error.captureStackTrace(error, this.autoScaleWriteCapacity);
}
throw error;
}
if (this.tableScaling.scalableWriteAttribute) {
throw new Error('Write AutoScaling already enabled for this table');
}
if (this.billingMode === BillingMode.PAY_PER_REQUEST) {
throw new Error('AutoScaling is not available for tables with PAY_PER_REQUEST billing mode');
}
this.tableScaling.scalableWriteAttribute = new scalable_table_attribute_1.ScalableTableAttribute(this, 'WriteScaling', {
serviceNamespace: appscaling.ServiceNamespace.DYNAMODB,
resourceId: `table/${this.tableName}`,
dimension: 'dynamodb:table:WriteCapacityUnits',
role: this.scalingRole,
...props,
});
for (const globalReplicaCustomResource of this.globalReplicaCustomResources) {
globalReplicaCustomResource.node.addDependency(this.tableScaling.scalableWriteAttribute);
}
return this.tableScaling.scalableWriteAttribute;
}
/**
* Enable read capacity scaling for the given GSI
*
* @returns An object to configure additional AutoScaling settings for this attribute
*/
autoScaleGlobalSecondaryIndexReadCapacity(indexName, props) {
try {
jsiiDeprecationWarnings._aws_cdk_aws_dynamodb_EnableScalingProps(props);
}
catch (error) {
if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") {
Error.captureStackTrace(error, this.autoScaleGlobalSecondaryIndexReadCapacity);
}
throw error;
}
if (this.billingMode === BillingMode.PAY_PER_REQUEST) {
throw new Error('AutoScaling is not available for tables with PAY_PER_REQUEST billing mode');
}
const attributePair = this.indexScaling.get(indexName);
if (!attributePair) {
throw new Error(`No global secondary index with name ${indexName}`);
}
if (attributePair.scalableReadAttribute) {
throw new Error('Read AutoScaling already enabled for this index');
}
return attributePair.scalableReadAttribute = new scalable_table_attribute_1.ScalableTableAttribute(this, `${indexName}ReadScaling`, {
serviceNamespace: appscaling.ServiceNamespace.DYNAMODB,
resourceId: `table/${this.tableName}/index/${indexName}`,
dimension: 'dynamodb:index:ReadCapacityUnits',
role: this.scalingRole,
...props,
});
}
/**
* Enable write capacity scaling for the given GSI
*
* @returns An object to configure additional AutoScaling settings for this attribute
*/
autoScaleGlobalSecondaryIndexWriteCapacity(indexName, props) {
try {
jsiiDeprecationWarnings._aws_cdk_aws_dynamodb_EnableScalingProps(props);
}
catch (error) {
if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") {
Error.captureStackTrace(error, this.autoScaleGlobalSecondaryIndexWriteCapacity);
}
throw error;
}
if (this.billingMode === BillingMode.PAY_PER_REQUEST) {
throw new Error('AutoScaling is not available for tables with PAY_PER_REQUEST billing mode');
}
const attributePair = this.indexScaling.get(indexName);
if (!attributePair) {
throw new Error(`No global secondary index with name ${indexName}`);
}
if (attributePair.scalableWriteAttribute) {
throw new Error('Write AutoScaling already enabled for this index');
}
return attributePair.scalableWriteAttribute = new scalable_table_attribute_1.ScalableTableAttribute(this, `${indexName}WriteScaling`, {
serviceNamespace: appscaling.ServiceNamespace.DYNAMODB,
resourceId: `table/${this.tableName}/index/${indexName}`,
dimension: 'dynamodb:index:WriteCapacityUnits',
role: this.scalingRole,
...props,
});
}
/**
* Get schema attributes of table or index.
*
* @returns Schema of table or index.
*/
schema(indexName) {
if (!indexName) {
return {
partitionKey: this.tablePartitionKey,
sortKey: this.tableSortKey,
};
}
let schema = this.secondaryIndexSchemas.get(indexName);
if (!schema) {
throw new Error(`Cannot find schema for index: ${indexName}. Use 'addGlobalSecondaryIndex' or 'addLocalSecondaryIndex' to add index`);
}
return schema;
}
/**
* Validate the table construct.
*
* @returns an array of validation error message
*/
validate() {
const errors = new Array();
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');
}
if (this.globalReplicaCustomResources.length > 0 && this.billingMode === BillingMode.PROVISIONED) {
const writeAutoScaleAttribute = this.tableScaling.scalableWriteAttribute;
if (!writeAutoScaleAttribute) {
errors.push('A global Table that uses PROVISIONED as the billing mode needs auto-scaled write capacity. ' +
'Use the autoScaleWriteCapacity() method to enable it.');
}
else if (!writeAutoScaleAttribute._scalingPolicyCreated) {
errors.push('A global Table that uses PROVISIONED as the billing mode needs auto-scaled write capacity with a policy. ' +
'Call one of the scaleOn*() methods of the object returned from autoScaleWriteCapacity()');
}
}
return errors;
}
/**
* Validate read and write capacity are not specified for on-demand tables (billing mode PAY_PER_REQUEST).
*
* @param props read and write capacity properties
*/
validateProvisioning(props) {
if (this.billingMode === BillingMode.PAY_PER_REQUEST) {
if (props.readCapacity !== undefined || props.writeCapacity !== undefined) {
throw new Error('you cannot provision read and write capacity for a table with PAY_PER_REQUEST billing mode');
}
}
}
/**
* Validate index name to check if a duplicate name already exists.
*
* @param indexName a name of global or local secondary index
*/
validateIndexName(indexName) {
if (this.secondaryIndexSchemas.has(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`);
}
}
/**
* Validate non-key attributes by checking limits within secondary index, which may vary in future.
*
* @param nonKeyAttributes a list of non-key attribute names
*/
validateNonKeyAttributes(nonKeyAttributes) {
if (this.nonKeyAttributes.size + nonKeyAttributes.length > 100) {
// 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 100');
}
// store all non-key attributes
nonKeyAttributes.forEach(att => this.nonKeyAttributes.add(att));
}
buildIndexKeySchema(partitionKey, sortKey) {
this.registerAttribute(partitionKey);
const indexKeySchema = [
{ attributeName: partitionKey.name, keyType: HASH_KEY_TYPE },
];
if (sortKey) {
this.registerAttribute(sortKey);
indexKeySchema.push({ attributeName: sortKey.name, keyType: RANGE_KEY_TYPE });
}
return indexKeySchema;
}
buildIndexProjection(props) {
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 ?? ProjectionType.ALL,
nonKeyAttributes: props.nonKeyAttributes ?? undefined,
};
}
findKey(keyType) {
return this.keySchema.find(prop => prop.keyType === keyType);
}
addKey(attribute, keyType) {
const existingProp = this.findKey(keyType);
if (existingProp) {
throw new Error(`Unable to set ${attribute.name} as a ${keyType} key, because ${existingProp.attributeName} is a ${keyType} key`);
}
this.registerAttribute(attribute);
this.keySchema.push({
attributeName: attribute.name,
keyType,
});
return this;
}
/**
* Register the key attribute of table or secondary index to assemble attribute definitions of TableResourceProps.
*
* @param attribute the key attribute of table or secondary index
*/
registerAttribute(attribute) {
const { name, type } = attribute;
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}`);
}
if (!existingDef) {
this.attributeDefinitions.push({
attributeName: name,
attributeType: type,
});
}
}
/**
* Return the role that will be used for AutoScaling
*/
makeScalingRole() {
// Use a Service Linked Role.
// https://docs.aws.amazon.com/autoscaling/application/userguide/application-auto-scaling-service-linked-roles.html
return iam.Role.fromRoleArn(this, 'ScalingRole', core_1.Stack.of(this).formatArn({
service: 'iam',
region: '',
resource: 'role/aws-service-role/dynamodb.application-autoscaling.amazonaws.com',
resourceName: 'AWSServiceRoleForApplicationAutoScaling_DynamoDBTable',
}));
}
/**
* Creates replica tables
*
* @param regions regions where to create tables
*/
createReplicaTables(regions, timeout, waitForReplicationToFinish) {
const stack = core_1.Stack.of(this);
if (!core_1.Token.isUnresolved(stack.region) && regions.includes(stack.region)) {
throw new Error('`replicationRegions` cannot include the region where this stack is deployed.');
}
const provider = replica_provider_1.ReplicaProvider.getOrCreate(this, { timeout });
// Documentation at https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/V2gt_IAM.html
// is currently incorrect. AWS Support recommends `dynamodb:*` in both source and destination regions
const onEventHandlerPolicy = new SourceTableAttachedPolicy(this, provider.onEventHandler.role);
const isCompleteHandlerPolicy = new SourceTableAttachedPolicy(this, provider.isCompleteHandler.role);
// Permissions in the source region
this.grant(onEventHandlerPolicy, 'dynamodb:*');
this.grant(isCompleteHandlerPolicy, 'dynamodb:DescribeTable');
let previousRegion;
let previousRegionCondition;
for (const region of new Set(regions)) { // Remove duplicates
// Use multiple custom resources because multiple create/delete
// updates cannot be combined in a single API call.
const currentRegion = new core_1.CustomResource(this, `Replica${region}`, {
serviceToken: provider.provider.serviceToken,
resourceType: 'Custom::DynamoDBReplica',
properties: {
TableName: this.tableName,
Region: region,
SkipReplicationCompletedWait: waitForReplicationToFinish == null
? undefined
// CFN changes Custom Resource properties to strings anyways,
// so let's do that ourselves to make it clear in the handler this is a string, not a boolean
: (!waitForReplicationToFinish).toString(),
},
});
currentRegion.node.addDependency(onEventHandlerPolicy.policy, isCompleteHandlerPolicy.policy);
this.globalReplicaCustomResources.push(currentRegion);
// Deploy time check to prevent from creating a replica in the region
// where this stack is deployed. Only needed for environment agnostic
// stacks.
let createReplica;
if (core_1.Token.isUnresolved(stack.region)) {
createReplica = new core_1.CfnCondition(this, `StackRegionNotEquals${region}`, {
expression: core_1.Fn.conditionNot(core_1.Fn.conditionEquals(region, core_1.Aws.REGION)),
});
const cfnCustomResource = currentRegion.node.defaultChild;
cfnCustomResource.cfnOptions.condition = createReplica;
}
// Save regional arns for grantXxx() methods
this.regionalArns.push(stack.formatArn({
region,
service: 'dynamodb',
resource: 'table',
resourceName: this.tableName,
}));
// We need to create/delete regions sequentially because we cannot
// have multiple table updates at the same time. The `isCompleteHandler`
// of the provider waits until the replica is in an ACTIVE state.
if (previousRegion) {
if (previousRegionCondition) {
// we can't simply use a Dependency,
// because the previousRegion is protected by the "different region" Condition,
// and you can't have Fn::If in DependsOn.
// Instead, rely on Ref adding a dependency implicitly!
const previousRegionCfnResource = previousRegion.node.defaultChild;
const currentRegionCfnResource = currentRegion.node.defaultChild;
currentRegionCfnResource.addMetadata('DynamoDbReplicationDependency', core_1.Fn.conditionIf(previousRegionCondition.logicalId, previousRegionCfnResource.ref, core_1.Aws.NO_VALUE));
}
else {
currentRegion.node.addDependency(previousRegion);
}
}
previousRegion = currentRegion;
previousRegionCondition = createReplica;
}
// Permissions in the destination regions (outside of the loop to
// minimize statements in the policy)
onEventHandlerPolicy.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['dynamodb:*'],
resources: this.regionalArns,
}));
}
/**
* Whether this table has indexes
*/
get hasIndex() {
return this.globalSecondaryIndexes.length + this.localSecondaryIndexes.length > 0;
}
/**
* Set up key properties and return the Table encryption property from the
* user's configuration.
*/
parseEncryption(props) {
let encryptionType = props.encryption;
if (encryptionType != null && props.serverSideEncryption != null) {
throw new Error('Only one of encryption and serverSideEncryption can be specified, but both were provided');
}
if (props.serverSideEncryption && props.encryptionKey) {
throw new Error('encryptionKey cannot be specified when serverSideEncryption is specified. Use encryption instead');
}
if (encryptionType === undefined) {
encryptionType = props.encryptionKey != null
// If there is a configured encryptionKey, the encryption is implicitly CUSTOMER_MANAGED
? TableEncryption.CUSTOMER_MANAGED
// Otherwise, if severSideEncryption is enabled, it's AWS_MANAGED; else undefined (do not set anything)
: props.serverSideEncryption ? TableEncryption.AWS_MANAGED : undefined;
}
if (encryptionType !== TableEncryption.CUSTOMER_MANAGED && props.encryptionKey) {
throw new Error('`encryptionKey cannot be specified unless encryption is set to TableEncryption.CUSTOMER_MANAGED (it was set to ${encryptionType})`');
}
if (encryptionType === TableEncryption.CUSTOMER_MANAGED && props.replicationRegions) {
throw new Error('TableEncryption.CUSTOMER_MANAGED is not supported by DynamoDB Global Tables (where replicationRegions was set)');
}
switch (encryptionType) {
case TableEncryption.CUSTOMER_MANAGED:
const encryptionKey = props.encryptionKey ?? new kms.Key(this, 'Key', {
description: `Customer-managed key auto-created for encrypting DynamoDB table at ${this.node.path}`,
enableKeyRotation: true,
});
return {
sseSpecification: { sseEnabled: true, kmsMasterKeyId: encryptionKey.keyArn, sseType: 'KMS' },
encryptionKey,
};
case TableEncryption.AWS_MANAGED:
// Not specifying "sseType: 'KMS'" here because it would cause phony changes to existing stacks.
return { sseSpecification: { sseEnabled: true } };
case TableEncryption.DEFAULT:
return { sseSpecification: { sseEnabled: false } };
case undefined:
// Not specifying "sseEnabled: false" here because it would cause phony changes to existing stacks.