Skip to content

Commit 06b463f

Browse files
feat(events): throw ValidationErrors instead of untyped Errors (#34316)
### 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 db62c5f commit 06b463f

File tree

9 files changed

+42
-39
lines changed

9 files changed

+42
-39
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const enableNoThrowDefaultErrorIn = [
5959
'aws-elasticloadbalancingv2',
6060
'aws-elasticloadbalancingv2-actions',
6161
'aws-elasticloadbalancingv2-targets',
62+
'aws-events',
6263
'aws-fsx',
6364
'aws-kinesis',
6465
'aws-kinesisfirehose',

packages/aws-cdk-lib/aws-events/lib/api-destination.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Construct } from 'constructs';
22
import { HttpMethod, IConnection } from './connection';
33
import { CfnApiDestination } from './events.generated';
4-
import { ArnFormat, IResource, Resource, Stack } from '../../core';
4+
import { ArnFormat, IResource, Resource, Stack, UnscopedValidationError } from '../../core';
55
import { addConstructMetadata } from '../../core/lib/metadata-resource';
66

77
/**
@@ -100,7 +100,7 @@ export class ApiDestination extends Resource implements IApiDestination {
100100
).resourceName;
101101

102102
if (!apiDestinationName) {
103-
throw new Error(`Could not extract Api Destionation name from ARN: '${attrs.apiDestinationArn}'`);
103+
throw new UnscopedValidationError(`Could not extract Api Destionation name from ARN: '${attrs.apiDestinationArn}'`);
104104
}
105105

106106
class Import extends Resource implements ApiDestination {

packages/aws-cdk-lib/aws-events/lib/connection.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Construct } from 'constructs';
22
import { CfnConnection } from './events.generated';
3-
import { IResource, Resource, Stack, SecretValue } from '../../core';
3+
import { IResource, Resource, Stack, SecretValue, UnscopedValidationError } from '../../core';
44
import { addConstructMetadata } from '../../core/lib/metadata-resource';
55

66
/**
@@ -100,7 +100,7 @@ export abstract class Authorization {
100100
*/
101101
public static oauth(props: OAuthAuthorizationProps): Authorization {
102102
if (![HttpMethod.POST, HttpMethod.GET, HttpMethod.PUT].includes(props.httpMethod)) {
103-
throw new Error('httpMethod must be one of GET, POST, PUT');
103+
throw new UnscopedValidationError('httpMethod must be one of GET, POST, PUT');
104104
}
105105

106106
return new class extends Authorization {

packages/aws-cdk-lib/aws-events/lib/event-bus.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { CfnEventBus, CfnEventBusPolicy } from './events.generated';
44
import * as iam from '../../aws-iam';
55
import * as kms from '../../aws-kms';
66
import * as sqs from '../../aws-sqs';
7-
import { Annotations, ArnFormat, FeatureFlags, IResource, Lazy, Names, Resource, Stack, Token } from '../../core';
7+
import { Annotations, ArnFormat, FeatureFlags, IResource, Lazy, Names, Resource, Stack, Token, UnscopedValidationError, ValidationError } from '../../core';
88
import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource';
99
import * as cxapi from '../../cx-api';
1010

@@ -311,23 +311,23 @@ export class EventBus extends EventBusBase {
311311
const eventBusNameRegex = /^[\/\.\-_A-Za-z0-9]{1,256}$/;
312312

313313
if (eventBusName !== undefined && eventSourceName !== undefined) {
314-
throw new Error(
314+
throw new UnscopedValidationError(
315315
'\'eventBusName\' and \'eventSourceName\' cannot both be provided',
316316
);
317317
}
318318

319319
if (eventBusName !== undefined) {
320320
if (!Token.isUnresolved(eventBusName)) {
321321
if (eventBusName === 'default') {
322-
throw new Error(
322+
throw new UnscopedValidationError(
323323
'\'eventBusName\' must not be \'default\'',
324324
);
325325
} else if (eventBusName.indexOf('/') > -1) {
326-
throw new Error(
326+
throw new UnscopedValidationError(
327327
'\'eventBusName\' must not contain \'/\'',
328328
);
329329
} else if (!eventBusNameRegex.test(eventBusName)) {
330-
throw new Error(
330+
throw new UnscopedValidationError(
331331
`'eventBusName' must satisfy: ${eventBusNameRegex}`,
332332
);
333333
}
@@ -340,11 +340,11 @@ export class EventBus extends EventBusBase {
340340
// Ex: aws.partner/PartnerName/acct1/repo1
341341
const eventSourceNameRegex = /^aws\.partner(\/[\.\-_A-Za-z0-9]+){2,}$/;
342342
if (!eventSourceNameRegex.test(eventSourceName)) {
343-
throw new Error(
343+
throw new UnscopedValidationError(
344344
`'eventSourceName' must satisfy: ${eventSourceNameRegex}`,
345345
);
346346
} else if (!eventBusNameRegex.test(eventSourceName)) {
347-
throw new Error(
347+
throw new UnscopedValidationError(
348348
`'eventSourceName' must satisfy: ${eventBusNameRegex}`,
349349
);
350350
}
@@ -387,7 +387,7 @@ export class EventBus extends EventBusBase {
387387
addConstructMetadata(this, props);
388388

389389
if (props?.description && !Token.isUnresolved(props.description) && props.description.length > 512) {
390-
throw new Error(`description must be less than or equal to 512 characters, got ${props.description.length}`);
390+
throw new ValidationError(`description must be less than or equal to 512 characters, got ${props.description.length}`, this);
391391
}
392392

393393
const eventBus = new CfnEventBus(this, 'Resource', {
@@ -446,7 +446,7 @@ export class EventBus extends EventBusBase {
446446
public addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult {
447447
// If no sid is provided, generate one based on the event bus id
448448
if (statement.sid == null) {
449-
throw new Error('Event Bus policy statements must have a sid');
449+
throw new ValidationError('Event Bus policy statements must have a sid', this);
450450
}
451451

452452
// In order to generate new statementIDs for the change in https://github.com/aws/aws-cdk/pull/27340

packages/aws-cdk-lib/aws-events/lib/event-pattern.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { captureStackTrace, IResolvable, IResolveContext, Token } from '../../core';
1+
import { captureStackTrace, IResolvable, IResolveContext, Token, UnscopedValidationError } from '../../core';
22

33
type ComparisonOperator = '>' | '>=' | '<' | '<=' | '=';
44

@@ -94,7 +94,7 @@ export class Match implements IResolvable {
9494
const ipv6Regex = /^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$/igm;
9595

9696
if (!ipv4Regex.test(range) && !ipv6Regex.test(range)) {
97-
throw new Error(`Invalid IP address range: ${range}`);
97+
throw new UnscopedValidationError(`Invalid IP address range: ${range}`);
9898
}
9999

100100
return this.fromObjects([{ cidr: range }]);
@@ -114,14 +114,14 @@ export class Match implements IResolvable {
114114
*/
115115
public static anythingBut(...values: any[]): string[] {
116116
if (values.length === 0) {
117-
throw new Error('anythingBut matchers must be non-empty lists');
117+
throw new UnscopedValidationError('anythingBut matchers must be non-empty lists');
118118
}
119119

120120
const allNumbers = values.every(v => typeof (v) === 'number');
121121
const allStrings = values.every(v => typeof (v) === 'string');
122122

123123
if (!(allNumbers || allStrings)) {
124-
throw new Error('anythingBut matchers must be lists that contain only strings or only numbers.');
124+
throw new UnscopedValidationError('anythingBut matchers must be lists that contain only strings or only numbers.');
125125
}
126126

127127
return this.fromObjects([{ 'anything-but': values }]);
@@ -200,7 +200,7 @@ export class Match implements IResolvable {
200200
*/
201201
public static interval(lower: number, upper: number): string[] {
202202
if (lower > upper) {
203-
throw new Error(`Invalid interval: [${lower}, ${upper}]`);
203+
throw new UnscopedValidationError(`Invalid interval: [${lower}, ${upper}]`);
204204
}
205205

206206
return Match.allOf(Match.greaterThanOrEqual(lower), Match.lessThanOrEqual(upper));
@@ -211,7 +211,7 @@ export class Match implements IResolvable {
211211
*/
212212
public static allOf(...matchers: any[]): string[] {
213213
if (matchers.length === 0) {
214-
throw new Error('A list of matchers must contain at least one element.');
214+
throw new UnscopedValidationError('A list of matchers must contain at least one element.');
215215
}
216216

217217
return this.fromMergedObjects(matchers);
@@ -222,14 +222,14 @@ export class Match implements IResolvable {
222222
*/
223223
public static anyOf(...matchers: any[]): string[] {
224224
if (matchers.length === 0) {
225-
throw new Error('A list of matchers must contain at least one element.');
225+
throw new UnscopedValidationError('A list of matchers must contain at least one element.');
226226
}
227227
return this.fromObjects(matchers);
228228
}
229229

230230
private static anythingButConjunction(filterKey: string, values: string[]): string[] {
231231
if (values.length === 0) {
232-
throw new Error('anythingBut matchers must be non-empty lists');
232+
throw new UnscopedValidationError('anythingBut matchers must be non-empty lists');
233233
}
234234

235235
// When there is a single value return it, otherwise return the array
@@ -266,7 +266,7 @@ export class Match implements IResolvable {
266266
// This is the only supported case for merging at the moment.
267267
// We can generalize this logic if EventBridge starts supporting more cases in the future.
268268
if (!matchers.every(matcher => matcher?.numeric)) {
269-
throw new Error('Only numeric matchers can be merged into a single matcher.');
269+
throw new UnscopedValidationError('Only numeric matchers can be merged into a single matcher.');
270270
}
271271

272272
return [{ numeric: matchers.flatMap(matcher => matcher.numeric) }];

packages/aws-cdk-lib/aws-events/lib/input.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { IRule } from './rule-ref';
22
import {
33
captureStackTrace, DefaultTokenResolver, IResolvable,
44
IResolveContext, Lazy, Stack, StringConcat, Token, Tokenization,
5+
UnscopedValidationError,
56
} from '../../core';
67

78
/**
@@ -169,7 +170,7 @@ export class FieldAwareEventInput extends RuleTargetInput {
169170

170171
const key = keyForField(t);
171172
if (inputPathsMap[key] && inputPathsMap[key] !== t.path) {
172-
throw new Error(`Single key '${key}' is used for two different JSON paths: '${t.path}' and '${inputPathsMap[key]}'`);
173+
throw new UnscopedValidationError(`Single key '${key}' is used for two different JSON paths: '${t.path}' and '${inputPathsMap[key]}'`);
173174
}
174175
inputPathsMap[key] = t.path;
175176

packages/aws-cdk-lib/aws-events/lib/rule.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Schedule } from './schedule';
88
import { IRuleTarget } from './target';
99
import { mergeEventPattern, renderEventPattern } from './util';
1010
import { IRole, PolicyStatement, Role, ServicePrincipal } from '../../aws-iam';
11-
import { App, IResource, Lazy, Names, Resource, Stack, Token, TokenComparison, PhysicalName, ArnFormat, Annotations } from '../../core';
11+
import { App, IResource, Lazy, Names, Resource, Stack, Token, TokenComparison, PhysicalName, ArnFormat, Annotations, ValidationError } from '../../core';
1212
import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource';
1313

1414
/**
@@ -107,7 +107,7 @@ export class Rule extends Resource implements IRule {
107107
addConstructMetadata(this, props);
108108

109109
if (props.eventBus && props.schedule) {
110-
throw new Error('Cannot associate rule with \'eventBus\' when using \'schedule\'');
110+
throw new ValidationError('Cannot associate rule with \'eventBus\' when using \'schedule\'', this);
111111
}
112112

113113
this.description = props.description;
@@ -184,27 +184,27 @@ export class Rule extends Resource implements IRule {
184184

185185
// for cross-account or cross-region events, we require a concrete target account and region
186186
if (!targetAccount || Token.isUnresolved(targetAccount)) {
187-
throw new Error('You need to provide a concrete account for the target stack when using cross-account or cross-region events');
187+
throw new ValidationError('You need to provide a concrete account for the target stack when using cross-account or cross-region events', this);
188188
}
189189
if (!targetRegion || Token.isUnresolved(targetRegion)) {
190-
throw new Error('You need to provide a concrete region for the target stack when using cross-account or cross-region events');
190+
throw new ValidationError('You need to provide a concrete region for the target stack when using cross-account or cross-region events', this);
191191
}
192192
if (Token.isUnresolved(sourceAccount)) {
193-
throw new Error('You need to provide a concrete account for the source stack when using cross-account or cross-region events');
193+
throw new ValidationError('You need to provide a concrete account for the source stack when using cross-account or cross-region events', this);
194194
}
195195

196196
// Don't exactly understand why this code was here (seems unlikely this rule would be violated), but
197197
// let's leave it in nonetheless.
198198
const sourceApp = this.node.root;
199199
if (!sourceApp || !App.isApp(sourceApp)) {
200-
throw new Error('Event stack which uses cross-account or cross-region targets must be part of a CDK app');
200+
throw new ValidationError('Event stack which uses cross-account or cross-region targets must be part of a CDK app', this);
201201
}
202202
const targetApp = Node.of(targetProps.targetResource).root;
203203
if (!targetApp || !App.isApp(targetApp)) {
204-
throw new Error('Target stack which uses cross-account or cross-region event targets must be part of a CDK app');
204+
throw new ValidationError('Target stack which uses cross-account or cross-region event targets must be part of a CDK app', this);
205205
}
206206
if (sourceApp !== targetApp) {
207-
throw new Error('Event stack and target stack must belong to the same CDK app');
207+
throw new ValidationError('Event stack and target stack must belong to the same CDK app', this);
208208
}
209209

210210
// The target of this Rule will be the default event bus of the target environment
@@ -432,7 +432,7 @@ export class Rule extends Resource implements IRule {
432432
}
433433

434434
// For now, we don't do the work for the support stack yet
435-
throw new Error('Cannot create a cross-account or cross-region rule for an imported resource (create a stack with the right environment for the imported resource)');
435+
throw new ValidationError('Cannot create a cross-account or cross-region rule for an imported resource (create a stack with the right environment for the imported resource)', this);
436436
}
437437

438438
/**

packages/aws-cdk-lib/aws-events/lib/schedule.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Construct } from 'constructs';
2-
import { Annotations, Duration } from '../../core';
2+
import { Annotations, Duration, UnscopedValidationError } from '../../core';
33

44
/**
55
* Schedule for scheduled event rules
@@ -27,12 +27,12 @@ export abstract class Schedule {
2727
if (duration.isUnresolved()) {
2828
const validDurationUnit = ['minute', 'minutes', 'hour', 'hours', 'day', 'days'];
2929
if (validDurationUnit.indexOf(duration.unitLabel()) === -1) {
30-
throw new Error("Allowed units for scheduling are: 'minute', 'minutes', 'hour', 'hours', 'day', 'days'");
30+
throw new UnscopedValidationError("Allowed units for scheduling are: 'minute', 'minutes', 'hour', 'hours', 'day', 'days'");
3131
}
3232
return new LiteralSchedule(`rate(${duration.formatTokenToNumber()})`);
3333
}
3434
if (duration.toMinutes() === 0) {
35-
throw new Error('Duration cannot be 0');
35+
throw new UnscopedValidationError('Duration cannot be 0');
3636
}
3737

3838
let rate = maybeRate(duration.toDays({ integral: false }), 'day');
@@ -46,7 +46,7 @@ export abstract class Schedule {
4646
*/
4747
public static cron(options: CronOptions): Schedule {
4848
if (options.weekDay !== undefined && options.day !== undefined) {
49-
throw new Error('Cannot supply both \'day\' and \'weekDay\', use at most one');
49+
throw new UnscopedValidationError('Cannot supply both \'day\' and \'weekDay\', use at most one');
5050
}
5151

5252
const minute = fallback(options.minute, '*');

packages/aws-cdk-lib/aws-events/lib/util.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { EventPattern } from './event-pattern';
2+
import { UnscopedValidationError } from '../../core';
23

34
/**
45
* Merge the `src` event pattern into the `dest` event pattern by adding all
@@ -15,7 +16,7 @@ export function mergeEventPattern(dest: any, src: any) {
1516

1617
function mergeObject(destObj: any, srcObj: any) {
1718
if (typeof(srcObj) !== 'object') {
18-
throw new Error(`Invalid event pattern '${JSON.stringify(srcObj)}', expecting an object or an array`);
19+
throw new UnscopedValidationError(`Invalid event pattern '${JSON.stringify(srcObj)}', expecting an object or an array`);
1920
}
2021

2122
for (const field of Object.keys(srcObj)) {
@@ -25,7 +26,7 @@ export function mergeEventPattern(dest: any, src: any) {
2526
if (srcValue === undefined) { continue; }
2627

2728
if (typeof(srcValue) !== 'object') {
28-
throw new Error(`Invalid event pattern field { ${field}: ${JSON.stringify(srcValue)} }. All fields must be arrays`);
29+
throw new UnscopedValidationError(`Invalid event pattern field { ${field}: ${JSON.stringify(srcValue)} }. All fields must be arrays`);
2930
}
3031

3132
// dest doesn't have this field
@@ -35,7 +36,7 @@ export function mergeEventPattern(dest: any, src: any) {
3536
}
3637

3738
if (Array.isArray(srcValue) !== Array.isArray(destValue)) {
38-
throw new Error(`Invalid event pattern field ${field}. ` +
39+
throw new UnscopedValidationError(`Invalid event pattern field ${field}. ` +
3940
`Type mismatch between existing pattern ${JSON.stringify(destValue)} and added pattern ${JSON.stringify(srcValue)}`);
4041
}
4142

0 commit comments

Comments
 (0)