Skip to content

Commit fa66705

Browse files
committed
feat(mixins-preview): strongly-typed ConstructSelector interface
Changed `Mixins.of()` to take a `IConstructSelector` instead of only one of the provided selectors. This allows users to implement custom selectors. Added `ConstructSelector.byPath()` to match on a construct path, to complement the existing `byId()`. BREAKING CHANGE: `ConstructSelector.byId()` now takes a glob pattern instead of a regular expression. BREAKING CHANGE: `ConstructSelector.resourcesOfType()` now must receive a string.
1 parent 5858006 commit fa66705

File tree

9 files changed

+163
-49
lines changed

9 files changed

+163
-49
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,9 @@
9595
"scripts/@aws-cdk/script-tests"
9696
],
9797
"nohoist": [
98+
"**/@types/glob",
9899
"**/jszip",
99100
"**/jszip/**",
100-
"**/@types/glob",
101101
"@aws-cdk/assertions-alpha/fs-extra",
102102
"@aws-cdk/assertions-alpha/fs-extra/**",
103103
"@aws-cdk/assertions/fs-extra",
@@ -146,6 +146,8 @@
146146
"@aws-cdk/core/yaml/**",
147147
"@aws-cdk/cx-api/semver",
148148
"@aws-cdk/cx-api/semver/**",
149+
"@aws-cdk/mixins-preview/minimatch",
150+
"@aws-cdk/mixins-preview/minimatch/**",
149151
"@aws-cdk/pipelines/aws-sdk",
150152
"@aws-cdk/pipelines/aws-sdk/**",
151153
"@aws-cdk/yaml-cfn/yaml",
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,93 @@
11
AWS Cloud Development Kit (AWS CDK)
22
Copyright 2018-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
4+
-------------------------------------------------------------------------------
5+
6+
The AWS CDK includes the following third-party software/licensing:
7+
8+
----------------
9+
10+
** minimatch - https://www.npmjs.com/package/minimatch
11+
Copyright (c) Isaac Z. Schlueter and Contributors
12+
13+
Permission to use, copy, modify, and/or distribute this software for any
14+
purpose with or without fee is hereby granted, provided that the above
15+
copyright notice and this permission notice appear in all copies.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
18+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
19+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
20+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
21+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
22+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
23+
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
24+
25+
----------------
26+
27+
** brace-expansion - https://www.npmjs.com/package/brace-expansion
28+
Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
29+
30+
Permission is hereby granted, free of charge, to any person obtaining a copy
31+
of this software and associated documentation files (the "Software"), to deal
32+
in the Software without restriction, including without limitation the rights
33+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
34+
copies of the Software, and to permit persons to whom the Software is
35+
furnished to do so, subject to the following conditions:
36+
37+
The above copyright notice and this permission notice shall be included in all
38+
copies or substantial portions of the Software.
39+
40+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
42+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
43+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
44+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
45+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
46+
SOFTWARE.
47+
48+
----------------
49+
50+
** balanced-match - https://www.npmjs.com/package/balanced-match
51+
Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
52+
53+
Permission is hereby granted, free of charge, to any person obtaining a copy
54+
of this software and associated documentation files (the "Software"), to deal
55+
in the Software without restriction, including without limitation the rights
56+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
57+
copies of the Software, and to permit persons to whom the Software is
58+
furnished to do so, subject to the following conditions:
59+
60+
The above copyright notice and this permission notice shall be included in all
61+
copies or substantial portions of the Software.
62+
63+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
64+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
65+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
66+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
67+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
68+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
69+
SOFTWARE.
70+
71+
----------------
72+
73+
** concat-map - https://www.npmjs.com/package/concat-map
74+
75+
Permission is hereby granted, free of charge, to any person obtaining a copy
76+
of this software and associated documentation files (the "Software"), to deal
77+
in the Software without restriction, including without limitation the rights
78+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
79+
copies of the Software, and to permit persons to whom the Software is
80+
furnished to do so, subject to the following conditions:
81+
82+
The above copyright notice and this permission notice shall be included in all
83+
copies or substantial portions of the Software.
84+
85+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
86+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
87+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
88+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
89+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
90+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
91+
SOFTWARE.
92+
93+
----------------

packages/@aws-cdk/mixins-preview/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,11 @@ Mixins operate on construct trees and can be applied selectively:
9595
Mixins.of(scope).apply(new EncryptionAtRest());
9696

9797
// Apply to specific resource types
98-
Mixins.of(scope, ConstructSelector.resourcesOfType(s3.CfnBucket))
98+
Mixins.of(scope, ConstructSelector.resourcesOfType(s3.CfnBucket.CFN_RESOURCE_TYPE_NAME))
9999
.apply(new EncryptionAtRest());
100100

101-
// Apply to constructs matching a pattern
102-
Mixins.of(scope, ConstructSelector.byId(/.*-prod-.*/))
101+
// Apply to constructs matching a path pattern
102+
Mixins.of(scope, ConstructSelector.byPath("**/*-prod-*/**"))
103103
.apply(new ProductionSecurityMixin());
104104
```
105105

packages/@aws-cdk/mixins-preview/lib/core/applicator.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import type { IConstruct } from 'constructs';
22
import { ValidationError } from 'aws-cdk-lib/core';
33
import type { IMixin } from './mixins';
4-
import { ConstructSelector } from './selectors';
4+
import { ConstructSelector, type IConstructSelector } from './selectors';
55

66
/**
77
* Applies mixins to constructs.
88
*/
99
export class MixinApplicator {
1010
private readonly scope: IConstruct;
11-
private readonly selector: ConstructSelector;
11+
private readonly selector: IConstructSelector;
1212

1313
constructor(
1414
scope: IConstruct,
15-
selector: ConstructSelector = ConstructSelector.all(),
15+
selector: IConstructSelector = ConstructSelector.all(),
1616
) {
1717
this.scope = scope;
1818
this.selector = selector;

packages/@aws-cdk/mixins-preview/lib/core/mixins.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { IConstruct } from 'constructs';
2-
import type { ConstructSelector } from './selectors';
2+
import type { IConstructSelector } from './selectors';
33
import { MixinApplicator } from './applicator';
44

55
// this will change when we update the interface to deliberately break compatibility checks
@@ -12,7 +12,7 @@ export class Mixins {
1212
/**
1313
* Creates a MixinApplicator for the given scope.
1414
*/
15-
static of(scope: IConstruct, selector?: ConstructSelector): MixinApplicator {
15+
static of(scope: IConstruct, selector?: IConstructSelector): MixinApplicator {
1616
return new MixinApplicator(scope, selector);
1717
}
1818
}

packages/@aws-cdk/mixins-preview/lib/core/selectors.ts

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,72 @@
1-
import type { IConstruct } from 'constructs';
1+
import type { IConstruct, Node } from 'constructs';
22
import { CfnResource } from 'aws-cdk-lib/core';
33

4+
/**
5+
* Selects constructs from a construct tree.
6+
*/
7+
export interface IConstructSelector {
8+
/**
9+
* Selects constructs from the given scope based on the selector's criteria.
10+
*/
11+
select(scope: IConstruct): IConstruct[];
12+
}
13+
414
/**
515
* Selects constructs from a construct tree based on various criteria.
616
*/
7-
export abstract class ConstructSelector {
17+
export class ConstructSelector {
818
/**
919
* Selects all constructs in the tree.
1020
*/
11-
static all(): ConstructSelector {
21+
static all(): IConstructSelector {
1222
return new AllConstructsSelector();
1323
}
1424

1525
/**
1626
* Selects CfnResource constructs or the default CfnResource child.
1727
*/
18-
static cfnResource(): ConstructSelector {
28+
static cfnResource(): IConstructSelector {
1929
return new CfnResourceSelector();
2030
}
2131

2232
/**
2333
* Selects only the provided construct.
2434
*/
25-
static onlyItself(): ConstructSelector {
35+
static onlyItself(): IConstructSelector {
2636
return new OnlyItselfSelector();
2737
}
2838

2939
/**
3040
* Selects constructs of a specific type.
3141
*/
32-
static resourcesOfType(type: string | any): ConstructSelector {
33-
return new ResourceTypeSelector(type);
42+
static resourcesOfType(...types: string[]): IConstructSelector {
43+
return new ResourceTypeSelector(types);
3444
}
3545

3646
/**
37-
* Selects constructs whose IDs match a pattern.
47+
* Selects constructs whose construct IDs match a pattern.
48+
* Uses glob like matching.
3849
*/
39-
static byId(pattern: any): ConstructSelector {
40-
return new IdPatternSelector(pattern);
50+
static byId(pattern: string): IConstructSelector {
51+
return new IdPatternSelector(pattern, 'id');
4152
}
4253

4354
/**
44-
* Selects constructs from the given scope based on the selector's criteria.
55+
* Selects constructs whose construct paths match a pattern.
56+
* Uses glob like matching.
4557
*/
46-
abstract select(scope: IConstruct): IConstruct[];
58+
static byPath(pattern: string): IConstructSelector {
59+
return new IdPatternSelector(pattern, 'path');
60+
}
4761
}
4862

49-
class AllConstructsSelector extends ConstructSelector {
63+
class AllConstructsSelector implements IConstructSelector {
5064
select(scope: IConstruct): IConstruct[] {
5165
return scope.node.findAll();
5266
}
5367
}
5468

55-
class CfnResourceSelector extends ConstructSelector {
69+
class CfnResourceSelector implements IConstructSelector {
5670
select(scope: IConstruct): IConstruct[] {
5771
if (CfnResource.isCfnResource(scope)) {
5872
return [scope];
@@ -65,26 +79,15 @@ class CfnResourceSelector extends ConstructSelector {
6579
}
6680
}
6781

68-
class ResourceTypeSelector extends ConstructSelector {
69-
constructor(private readonly type: string | any) {
70-
super();
82+
class ResourceTypeSelector implements IConstructSelector {
83+
constructor(private readonly types: string[]) {
7184
}
7285

7386
select(scope: IConstruct): IConstruct[] {
7487
const result: IConstruct[] = [];
7588
const visit = (node: IConstruct) => {
76-
if (typeof this.type === 'string') {
77-
if (CfnResource.isCfnResource(node) && node.cfnResourceType === this.type) {
78-
result.push(node);
79-
}
80-
} else if ('isCfnResource' in this.type && 'CFN_RESOURCE_TYPE_NAME' in this.type) {
81-
if (CfnResource.isCfnResource(node) && node.cfnResourceType === this.type.CFN_RESOURCE_TYPE_NAME) {
82-
result.push(node);
83-
}
84-
} else {
85-
if (node instanceof this.type) {
86-
result.push(node);
87-
}
89+
if (CfnResource.isCfnResource(node) && this.types.includes(node.cfnResourceType)) {
90+
result.push(node);
8891
}
8992
for (const child of node.node.children) {
9093
visit(child);
@@ -95,15 +98,17 @@ class ResourceTypeSelector extends ConstructSelector {
9598
}
9699
}
97100

98-
class IdPatternSelector extends ConstructSelector {
99-
constructor(private readonly pattern: any) {
100-
super();
101-
}
101+
// Must be a 'require' to not run afoul of ESM module import rules
102+
// eslint-disable-next-line @typescript-eslint/no-require-imports
103+
const minimatch = require('minimatch');
104+
105+
class IdPatternSelector implements IConstructSelector {
106+
constructor(private readonly pattern: string, private field: keyof Node) {}
102107

103108
select(scope: IConstruct): IConstruct[] {
104109
const result: IConstruct[] = [];
105110
const visit = (node: IConstruct) => {
106-
if (this.pattern && typeof this.pattern.test === 'function' && this.pattern.test(node.node.id)) {
111+
if (minimatch(node.node[this.field], this.pattern)) {
107112
result.push(node);
108113
}
109114
for (const child of node.node.children) {
@@ -115,7 +120,7 @@ class IdPatternSelector extends ConstructSelector {
115120
}
116121
}
117122

118-
class OnlyItselfSelector extends ConstructSelector {
123+
class OnlyItselfSelector implements IConstructSelector {
119124
select(scope: IConstruct): IConstruct[] {
120125
return [scope];
121126
}

packages/@aws-cdk/mixins-preview/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,7 @@
648648
"url": "https://aws.amazon.com",
649649
"organization": true
650650
},
651+
"homepage": "https://github.com/aws/aws-cdk",
651652
"license": "Apache-2.0",
652653
"devDependencies": {
653654
"@aws-cdk/cdk-build-tools": "0.0.0",
@@ -665,7 +666,12 @@
665666
"jest": "^29.7.0",
666667
"tsx": "^4.20.6"
667668
},
668-
"homepage": "https://github.com/aws/aws-cdk",
669+
"dependencies": {
670+
"minimatch": "^3.1.2"
671+
},
672+
"bundleDependencies": [
673+
"minimatch"
674+
],
669675
"peerDependencies": {
670676
"aws-cdk-lib": "^0.0.0",
671677
"constructs": "^10.0.0"

packages/@aws-cdk/mixins-preview/test/core/selectors.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ describe('ConstructSelector', () => {
3333
const bucket = new s3.CfnBucket(stack, 'Bucket');
3434
const logGroup = new logs.CfnLogGroup(stack, 'LogGroup');
3535

36-
const selected = ConstructSelector.resourcesOfType(s3.CfnBucket).select(stack);
36+
const selected = ConstructSelector.resourcesOfType(s3.CfnBucket.CFN_RESOURCE_TYPE_NAME).select(stack);
3737
expect(selected).toContain(bucket);
3838
expect(selected).not.toContain(logGroup);
3939
});
@@ -51,7 +51,17 @@ describe('ConstructSelector', () => {
5151
const prodBucket = new s3.CfnBucket(stack, 'prod-bucket');
5252
const devBucket = new s3.CfnBucket(stack, 'dev-bucket');
5353

54-
const selected = ConstructSelector.byId(/prod-.*/).select(stack);
54+
const selected = ConstructSelector.byId('*prod*').select(stack);
55+
expect(selected).toContain(prodBucket);
56+
expect(selected).not.toContain(devBucket);
57+
});
58+
59+
test('byPath() selects by construct path pattern', () => {
60+
const scope = new Construct(stack, 'Prefix');
61+
const prodBucket = new s3.CfnBucket(scope, 'prod-bucket');
62+
const devBucket = new s3.CfnBucket(stack, 'dev-bucket');
63+
64+
const selected = ConstructSelector.byPath('*/Prefix/**').select(stack);
5565
expect(selected).toContain(prodBucket);
5666
expect(selected).not.toContain(devBucket);
5767
});

packages/@aws-cdk/mixins-preview/test/mixins/full-example.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ describe('Integration Tests', () => {
2525
// Apply encryption only to production buckets
2626
Mixins.of(
2727
stack,
28-
ConstructSelector.byId(/.*Prod.*/),
28+
ConstructSelector.byId('*Prod*'),
2929
).apply(new s3Mixins.AutoDeleteObjects());
3030

3131
// Apply versioning to all S3 buckets
3232
Mixins.of(
3333
stack,
34-
ConstructSelector.resourcesOfType(s3.CfnBucket),
34+
ConstructSelector.resourcesOfType(s3.CfnBucket.CFN_RESOURCE_TYPE_NAME),
3535
).apply(new s3Mixins.EnableVersioning());
3636

3737
// Verify auto-delete only applied to prod bucket

0 commit comments

Comments
 (0)