Skip to content

Commit a9bae27

Browse files
authored
feat(dynamodb): throw ValidationErrors instead of untyped Errors (#33871)
### Issue # (if applicable) Relates to #32569 ### Reason for this change untyped Errors are not recommended ### Description of changes ValidationErrors everywhere ### Describe any new or updated permissions being added None ### Description of how you validated changes Existing tests. Exemptions granted as this is a refactor of existing code. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 5bc9292 commit a9bae27

File tree

6 files changed

+89
-81
lines changed

6 files changed

+89
-81
lines changed

packages/aws-cdk-lib/.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const enableNoThrowDefaultErrorIn = [
4848
'aws-cognito',
4949
'aws-config',
5050
'aws-docdb',
51+
'aws-dynamodb',
5152
'aws-ecr',
5253
'aws-efs',
5354
'aws-elasticloadbalancing',

packages/aws-cdk-lib/aws-dynamodb/lib/capacity.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { CfnGlobalTable } from './dynamodb.generated';
2+
import { UnscopedValidationError } from '../../core';
23

34
/**
45
* Capacity modes
@@ -71,7 +72,7 @@ export abstract class Capacity {
7172
}
7273

7374
public _renderWriteCapacity() {
74-
throw new Error(`You cannot configure 'writeCapacity' with ${CapacityMode.FIXED} capacity mode`);
75+
throw new UnscopedValidationError(`You cannot configure 'writeCapacity' with ${CapacityMode.FIXED} capacity mode`);
7576
}
7677
}) (CapacityMode.FIXED);
7778
}
@@ -88,15 +89,15 @@ export abstract class Capacity {
8889
super(mode);
8990

9091
if ((options.minCapacity ?? 1) > options.maxCapacity) {
91-
throw new Error('`minCapacity` must be less than or equal to `maxCapacity`');
92+
throw new UnscopedValidationError('`minCapacity` must be less than or equal to `maxCapacity`');
9293
}
9394

9495
if (options.targetUtilizationPercent !== undefined && (options.targetUtilizationPercent < 20 || options.targetUtilizationPercent > 90)) {
95-
throw new Error('`targetUtilizationPercent` cannot be less than 20 or greater than 90');
96+
throw new UnscopedValidationError('`targetUtilizationPercent` cannot be less than 20 or greater than 90');
9697
}
9798

9899
if (options.seedCapacity !== undefined && (options.seedCapacity < 1)) {
99-
throw new Error(`'seedCapacity' cannot be less than 1 - received ${options.seedCapacity}`);
100+
throw new UnscopedValidationError(`'seedCapacity' cannot be less than 1 - received ${options.seedCapacity}`);
100101
}
101102
}
102103

packages/aws-cdk-lib/aws-dynamodb/lib/encryption.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Construct } from 'constructs';
22
import { CfnGlobalTable } from './dynamodb.generated';
33
import { TableEncryption } from './shared';
44
import { IKey } from '../../aws-kms';
5-
import { Stack, Token } from '../../core';
5+
import { Stack, Token, ValidationError } from '../../core';
66

77
/**
88
* Represents server-side encryption for a DynamoDB table.
@@ -61,11 +61,11 @@ export abstract class TableEncryptionV2 {
6161
public _renderReplicaSseSpecification(scope: Construct, replicaRegion: string) {
6262
const stackRegion = Stack.of(scope).region;
6363
if (Token.isUnresolved(stackRegion)) {
64-
throw new Error('Replica SSE specification cannot be rendered in a region agnostic stack');
64+
throw new ValidationError('Replica SSE specification cannot be rendered in a region agnostic stack', scope);
6565
}
6666

6767
if (replicaKeyArns.hasOwnProperty(stackRegion)) {
68-
throw new Error(`KMS key for deployment region ${stackRegion} cannot be defined in 'replicaKeyArns'`);
68+
throw new ValidationError(`KMS key for deployment region ${stackRegion} cannot be defined in 'replicaKeyArns'`, scope);
6969
}
7070

7171
if (replicaRegion === stackRegion) {
@@ -76,7 +76,7 @@ export abstract class TableEncryptionV2 {
7676

7777
const regionInReplicaKeyArns = replicaKeyArns.hasOwnProperty(replicaRegion);
7878
if (!regionInReplicaKeyArns) {
79-
throw new Error(`KMS key for ${replicaRegion} was not found in 'replicaKeyArns'`);
79+
throw new ValidationError(`KMS key for ${replicaRegion} was not found in 'replicaKeyArns'`, scope);
8080
}
8181

8282
return {

packages/aws-cdk-lib/aws-dynamodb/lib/table-v2-base.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Operation, SystemErrorsForOperationsMetricOptions, OperationsMetricOpti
44
import { IMetric, MathExpression, Metric, MetricOptions, MetricProps } from '../../aws-cloudwatch';
55
import { AddToResourcePolicyResult, Grant, IGrantable, IResourceWithPolicy, PolicyDocument, PolicyStatement } from '../../aws-iam';
66
import { IKey } from '../../aws-kms';
7-
import { Resource } from '../../core';
7+
import { Resource, ValidationError } from '../../core';
88

99
/**
1010
* Represents an instance of a DynamoDB table.
@@ -95,7 +95,7 @@ export abstract class TableBaseV2 extends Resource implements ITableV2, IResourc
9595
*/
9696
public grantStream(grantee: IGrantable, ...actions: string[]): Grant {
9797
if (!this.tableStreamArn) {
98-
throw new Error(`No stream ARN found on the table ${this.node.path}`);
98+
throw new ValidationError(`No stream ARN found on the table ${this.node.path}`, this);
9999
}
100100

101101
return Grant.addToPrincipal({
@@ -131,7 +131,7 @@ export abstract class TableBaseV2 extends Resource implements ITableV2, IResourc
131131
*/
132132
public grantTableListStreams(grantee: IGrantable): Grant {
133133
if (!this.tableStreamArn) {
134-
throw new Error(`No stream ARN found on the table ${this.node.path}`);
134+
throw new ValidationError(`No stream ARN found on the table ${this.node.path}`, this);
135135
}
136136

137137
return Grant.addToPrincipal({
@@ -257,7 +257,7 @@ export abstract class TableBaseV2 extends Resource implements ITableV2, IResourc
257257
*/
258258
public metricUserErrors(props?: MetricOptions): Metric {
259259
if (props?.dimensions) {
260-
throw new Error('`dimensions` is not supported for the `UserErrors` metric');
260+
throw new ValidationError('`dimensions` is not supported for the `UserErrors` metric', this);
261261
}
262262

263263
return this.metric('UserErrors', { statistic: 'sum', ...props, dimensionsMap: {} });
@@ -281,7 +281,7 @@ export abstract class TableBaseV2 extends Resource implements ITableV2, IResourc
281281
*/
282282
public metricSuccessfulRequestLatency(props?: MetricOptions): Metric {
283283
if (!props?.dimensions?.Operation && !props?.dimensionsMap?.Operation) {
284-
throw new Error('`Operation` dimension must be passed for the `SuccessfulRequestLatency` metric');
284+
throw new ValidationError('`Operation` dimension must be passed for the `SuccessfulRequestLatency` metric', this);
285285
}
286286

287287
const dimensionsMap = {
@@ -351,7 +351,7 @@ export abstract class TableBaseV2 extends Resource implements ITableV2, IResourc
351351
public metricSystemErrors(props?: MetricOptions): Metric {
352352
if (!props?.dimensions?.Operation && !props?.dimensionsMap?.Operation) {
353353
// 'Operation' must be passed because its an operational metric.
354-
throw new Error("'Operation' dimension must be passed for the 'SystemErrors' metric.");
354+
throw new ValidationError("'Operation' dimension must be passed for the 'SystemErrors' metric.", this);
355355
}
356356

357357
const dimensionsMap = {
@@ -368,7 +368,7 @@ export abstract class TableBaseV2 extends Resource implements ITableV2, IResourc
368368
*/
369369
private sumMetricsForOperations(metricName: string, expressionLabel: string, props?: OperationsMetricOptions) {
370370
if (props?.dimensions?.Operation) {
371-
throw new Error('The Operation dimension is not supported. Use the `operations` property');
371+
throw new ValidationError('The Operation dimension is not supported. Use the `operations` property', this);
372372
}
373373

374374
const operations = props?.operations ?? Object.values(Operation);
@@ -396,7 +396,7 @@ export abstract class TableBaseV2 extends Resource implements ITableV2, IResourc
396396
const mapper = metricNameMapper ?? (op => op.toLowerCase());
397397

398398
if (props?.dimensions?.Operation) {
399-
throw new Error('Invalid properties. Operation dimension is not supported when calculating operational metrics');
399+
throw new ValidationError('Invalid properties. Operation dimension is not supported when calculating operational metrics', this);
400400
}
401401

402402
for (const operation of operations) {
@@ -408,7 +408,7 @@ export abstract class TableBaseV2 extends Resource implements ITableV2, IResourc
408408
const operationMetricName = mapper(operation);
409409
const firstChar = operationMetricName.charAt(0);
410410
if (firstChar === firstChar.toUpperCase()) {
411-
throw new Error(`Mapper generated an illegal operation metric name: ${operationMetricName}. Must start with a lowercase letter`);
411+
throw new ValidationError(`Mapper generated an illegal operation metric name: ${operationMetricName}. Must start with a lowercase letter`, this);
412412
}
413413

414414
metrics[operationMetricName] = metric;
@@ -441,7 +441,7 @@ export abstract class TableBaseV2 extends Resource implements ITableV2, IResourc
441441

442442
if (options.streamActions) {
443443
if (!this.tableStreamArn) {
444-
throw new Error(`No stream ARNs found on the table ${this.node.path}`);
444+
throw new ValidationError(`No stream ARNs found on the table ${this.node.path}`, this);
445445
}
446446

447447
return Grant.addToPrincipal({
@@ -452,7 +452,7 @@ export abstract class TableBaseV2 extends Resource implements ITableV2, IResourc
452452
});
453453
}
454454

455-
throw new Error(`Unexpected 'action', ${options.tableActions || options.streamActions}`);
455+
throw new ValidationError(`Unexpected 'action', ${options.tableActions || options.streamActions}`, this);
456456
}
457457

458458
private configureMetric(props: MetricProps) {

packages/aws-cdk-lib/aws-dynamodb/lib/table-v2.ts

+23-23
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ export class TableV2 extends TableBaseV2 {
454454

455455
const resourceRegion = stack.splitArn(tableArn, ArnFormat.SLASH_RESOURCE_NAME).region;
456456
if (!resourceRegion) {
457-
throw new Error('Table ARN must be of the form: arn:<partition>:dynamodb:<region>:<account>:table/<table-name>');
457+
throw new ValidationError('Table ARN must be of the form: arn:<partition>:dynamodb:<region>:<account>:table/<table-name>', this);
458458
}
459459

460460
this.region = resourceRegion;
@@ -472,7 +472,7 @@ export class TableV2 extends TableBaseV2 {
472472
const stack = Stack.of(scope);
473473
if (!attrs.tableArn) {
474474
if (!attrs.tableName) {
475-
throw new Error('At least one of `tableArn` or `tableName` must be provided');
475+
throw new ValidationError('At least one of `tableArn` or `tableName` must be provided', scope);
476476
}
477477

478478
tableName = attrs.tableName;
@@ -483,13 +483,13 @@ export class TableV2 extends TableBaseV2 {
483483
});
484484
} else {
485485
if (attrs.tableName) {
486-
throw new Error('Only one of `tableArn` or `tableName` can be provided, but not both');
486+
throw new ValidationError('Only one of `tableArn` or `tableName` can be provided, but not both', scope);
487487
}
488488

489489
tableArn = attrs.tableArn;
490490
const resourceName = stack.splitArn(tableArn, ArnFormat.SLASH_RESOURCE_NAME).resourceName;
491491
if (!resourceName) {
492-
throw new Error('Table ARN must be of the form: arn:<partition>:dynamodb:<region>:<account>:table/<table-name>');
492+
throw new ValidationError('Table ARN must be of the form: arn:<partition>:dynamodb:<region>:<account>:table/<table-name>', scope);
493493
}
494494
tableName = resourceName;
495495
}
@@ -691,19 +691,19 @@ export class TableV2 extends TableBaseV2 {
691691
@MethodMetadata()
692692
public replica(region: string): ITableV2 {
693693
if (Token.isUnresolved(this.stack.region)) {
694-
throw new Error('Replica tables are not supported in a region agnostic stack');
694+
throw new ValidationError('Replica tables are not supported in a region agnostic stack', this);
695695
}
696696

697697
if (Token.isUnresolved(region)) {
698-
throw new Error('Provided `region` cannot be a token');
698+
throw new ValidationError('Provided `region` cannot be a token', this);
699699
}
700700

701701
if (region === this.stack.region) {
702702
return this;
703703
}
704704

705705
if (!this.replicaTables.has(region)) {
706-
throw new Error(`No replica table exists in region ${region}`);
706+
throw new ValidationError(`No replica table exists in region ${region}`, this);
707707
}
708708

709709
const replicaTableArn = this.replicaTableArns.find(arn => arn.includes(region));
@@ -874,7 +874,7 @@ export class TableV2 extends TableBaseV2 {
874874

875875
props.nonKeyAttributes?.forEach(attr => this.nonKeyAttributes.add(attr));
876876
if (this.nonKeyAttributes.size > MAX_NON_KEY_ATTRIBUTES) {
877-
throw new Error(`The maximum number of 'nonKeyAttributes' across all secondary indexes is ${MAX_NON_KEY_ATTRIBUTES}`);
877+
throw new ValidationError(`The maximum number of 'nonKeyAttributes' across all secondary indexes is ${MAX_NON_KEY_ATTRIBUTES}`, this);
878878
}
879879

880880
return {
@@ -938,7 +938,7 @@ export class TableV2 extends TableBaseV2 {
938938

939939
const existingAttributeDef = this.attributeDefinitions.find(def => def.attributeName === name);
940940
if (existingAttributeDef && existingAttributeDef.attributeType !== type) {
941-
throw new Error(`Unable to specify ${name} as ${type} because it was already defined as ${existingAttributeDef.attributeType}`);
941+
throw new ValidationError(`Unable to specify ${name} as ${type} because it was already defined as ${existingAttributeDef.attributeType}`, this);
942942
}
943943

944944
if (!existingAttributeDef) {
@@ -952,77 +952,77 @@ export class TableV2 extends TableBaseV2 {
952952

953953
private validateIndexName(indexName: string) {
954954
if (this.globalSecondaryIndexes.has(indexName) || this.localSecondaryIndexes.has(indexName)) {
955-
throw new Error(`Duplicate secondary index name, ${indexName}, is not allowed`);
955+
throw new ValidationError(`Duplicate secondary index name, ${indexName}, is not allowed`, this);
956956
}
957957
}
958958

959959
private validateIndexProjection(props: SecondaryIndexProps) {
960960
if (props.projectionType === ProjectionType.INCLUDE && !props.nonKeyAttributes) {
961-
throw new Error(`Non-key attributes should be specified when using ${ProjectionType.INCLUDE} projection type`);
961+
throw new ValidationError(`Non-key attributes should be specified when using ${ProjectionType.INCLUDE} projection type`, this);
962962
}
963963

964964
if (props.projectionType !== ProjectionType.INCLUDE && props.nonKeyAttributes) {
965-
throw new Error(`Non-key attributes should not be specified when not using ${ProjectionType.INCLUDE} projection type`);
965+
throw new ValidationError(`Non-key attributes should not be specified when not using ${ProjectionType.INCLUDE} projection type`, this);
966966
}
967967
}
968968

969969
private validateReplicaIndexOptions(options: { [indexName: string]: ReplicaGlobalSecondaryIndexOptions }) {
970970
for (const indexName of Object.keys(options)) {
971971
if (!this.globalSecondaryIndexes.has(indexName)) {
972-
throw new Error(`Cannot configure replica global secondary index, ${indexName}, because it is not defined on the primary table`);
972+
throw new ValidationError(`Cannot configure replica global secondary index, ${indexName}, because it is not defined on the primary table`, this);
973973
}
974974

975975
const replicaGsiOptions = options[indexName];
976976
if (this.billingMode === BillingMode.PAY_PER_REQUEST && replicaGsiOptions.readCapacity) {
977-
throw new Error(`Cannot configure 'readCapacity' for replica global secondary index, ${indexName}, because billing mode is ${BillingMode.PAY_PER_REQUEST}`);
977+
throw new ValidationError(`Cannot configure 'readCapacity' for replica global secondary index, ${indexName}, because billing mode is ${BillingMode.PAY_PER_REQUEST}`, this);
978978
}
979979
}
980980
}
981981

982982
private validateReplica(props: ReplicaTableProps) {
983983
const stackRegion = this.stack.region;
984984
if (Token.isUnresolved(stackRegion)) {
985-
throw new Error('Replica tables are not supported in a region agnostic stack');
985+
throw new ValidationError('Replica tables are not supported in a region agnostic stack', this);
986986
}
987987

988988
if (Token.isUnresolved(props.region)) {
989-
throw new Error('Replica table region must not be a token');
989+
throw new ValidationError('Replica table region must not be a token', this);
990990
}
991991

992992
if (props.region === this.stack.region) {
993-
throw new Error(`You cannot add a replica table in the same region as the primary table - the primary table region is ${this.region}`);
993+
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);
994994
}
995995

996996
if (this.replicaTables.has(props.region)) {
997-
throw new Error(`Duplicate replica table region, ${props.region}, is not allowed`);
997+
throw new ValidationError(`Duplicate replica table region, ${props.region}, is not allowed`, this);
998998
}
999999

10001000
if (this.billingMode === BillingMode.PAY_PER_REQUEST && props.readCapacity) {
1001-
throw new Error(`You cannot provide 'readCapacity' on a replica table when the billing mode is ${BillingMode.PAY_PER_REQUEST}`);
1001+
throw new ValidationError(`You cannot provide 'readCapacity' on a replica table when the billing mode is ${BillingMode.PAY_PER_REQUEST}`, this);
10021002
}
10031003
}
10041004

10051005
private validateGlobalSecondaryIndex(props: GlobalSecondaryIndexPropsV2) {
10061006
this.validateIndexName(props.indexName);
10071007

10081008
if (this.globalSecondaryIndexes.size === MAX_GSI_COUNT) {
1009-
throw new Error(`You may not provide more than ${MAX_GSI_COUNT} global secondary indexes`);
1009+
throw new ValidationError(`You may not provide more than ${MAX_GSI_COUNT} global secondary indexes`, this);
10101010
}
10111011

10121012
if (this.billingMode === BillingMode.PAY_PER_REQUEST && (props.readCapacity || props.writeCapacity)) {
1013-
throw new Error(`You cannot configure 'readCapacity' or 'writeCapacity' on a global secondary index when the billing mode is ${BillingMode.PAY_PER_REQUEST}`);
1013+
throw new ValidationError(`You cannot configure 'readCapacity' or 'writeCapacity' on a global secondary index when the billing mode is ${BillingMode.PAY_PER_REQUEST}`, this);
10141014
}
10151015
}
10161016

10171017
private validateLocalSecondaryIndex(props: LocalSecondaryIndexProps) {
10181018
this.validateIndexName(props.indexName);
10191019

10201020
if (!this.hasSortKey) {
1021-
throw new Error('The table must have a sort key in order to add a local secondary index');
1021+
throw new ValidationError('The table must have a sort key in order to add a local secondary index', this);
10221022
}
10231023

10241024
if (this.localSecondaryIndexes.size === MAX_LSI_COUNT) {
1025-
throw new Error(`You may not provide more than ${MAX_LSI_COUNT} local secondary indexes`);
1025+
throw new ValidationError(`You may not provide more than ${MAX_LSI_COUNT} local secondary indexes`, this);
10261026
}
10271027
}
10281028

0 commit comments

Comments
 (0)