Skip to content

Commit 08f0a39

Browse files
committed
detached-construct
1 parent 24d2adf commit 08f0a39

File tree

7 files changed

+114
-54
lines changed

7 files changed

+114
-54
lines changed

packages/aws-cdk-lib/aws-cloudfront/lib/cache-policy.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
import { Construct, Node } from 'constructs';
1+
import { Construct } from 'constructs';
22
import { CachePolicyReference, CfnCachePolicy, ICachePolicyRef } from './cloudfront.generated';
33
import {
44
Duration,
55
Names,
66
Resource,
7-
ResourceEnvironment,
87
Stack,
98
Token,
109
UnscopedValidationError,
1110
ValidationError,
1211
withResolved,
1312
} from '../../core';
1413
import { addConstructMetadata } from '../../core/lib/metadata-resource';
14+
import { DetachedConstruct } from '../../core/lib/private/detached-construct';
1515
import { propertyInjectable } from '../../core/lib/prop-injectable';
1616

1717
/**
@@ -148,19 +148,14 @@ export class CachePolicy extends Resource implements ICachePolicy {
148148

149149
/** Use an existing managed cache policy. */
150150
private static fromManagedCachePolicy(managedCachePolicyId: string): ICachePolicy {
151-
return new class implements ICachePolicy {
152-
public get node(): Node {
153-
throw new UnscopedValidationError('The result of fromManagedCachePolicy can not be used in this API');
154-
}
155-
156-
public get env(): ResourceEnvironment {
157-
throw new UnscopedValidationError('The result of fromManagedCachePolicy can not be used in this API');
158-
}
159-
151+
return new class extends DetachedConstruct implements ICachePolicy {
160152
public readonly cachePolicyId = managedCachePolicyId;
161153
public readonly cachePolicyRef = {
162154
cachePolicyId: managedCachePolicyId,
163155
};
156+
constructor() {
157+
super('The result of fromManagedCachePolicy can not be used in this API');
158+
}
164159
}();
165160
}
166161

packages/aws-cdk-lib/aws-cloudfront/lib/origin-request-policy.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { Construct, Node } from 'constructs';
1+
import { Construct } from 'constructs';
22
import { CfnOriginRequestPolicy, IOriginRequestPolicyRef, OriginRequestPolicyReference } from './cloudfront.generated';
3-
import { Names, Resource, ResourceEnvironment, Token, UnscopedValidationError, ValidationError } from '../../core';
3+
import { Names, Resource, Token, UnscopedValidationError, ValidationError } from '../../core';
44
import { addConstructMetadata } from '../../core/lib/metadata-resource';
5+
import { DetachedConstruct } from '../../core/lib/private/detached-construct';
56
import { propertyInjectable } from '../../core/lib/prop-injectable';
67

78
/**
@@ -87,19 +88,14 @@ export class OriginRequestPolicy extends Resource implements IOriginRequestPolic
8788

8889
/** Use an existing managed origin request policy. */
8990
private static fromManagedOriginRequestPolicy(managedOriginRequestPolicyId: string): IOriginRequestPolicy {
90-
return new class implements IOriginRequestPolicy {
91-
public get node(): Node {
92-
throw new UnscopedValidationError('The result of fromManagedOriginRequestPolicy can not be used in this API');
93-
}
94-
95-
public get env(): ResourceEnvironment {
96-
throw new UnscopedValidationError('The result of fromManagedOriginRequestPolicy can not be used in this API');
97-
}
98-
91+
return new class extends DetachedConstruct implements IOriginRequestPolicy {
9992
public readonly originRequestPolicyId = managedOriginRequestPolicyId;
10093
public readonly originRequestPolicyRef = {
10194
originRequestPolicyId: managedOriginRequestPolicyId,
10295
};
96+
constructor() {
97+
super('The result of fromManagedOriginRequestPolicy can not be used in this API');
98+
}
10399
}();
104100
}
105101

packages/aws-cdk-lib/aws-cloudfront/lib/response-headers-policy.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { Construct, Node } from 'constructs';
1+
import { Construct } from 'constructs';
22
import {
33
CfnResponseHeadersPolicy,
44
IResponseHeadersPolicyRef,
55
ResponseHeadersPolicyReference,
66
} from './cloudfront.generated';
7-
import { Duration, Names, Resource, ResourceEnvironment, Token, UnscopedValidationError, ValidationError, withResolved } from '../../core';
7+
import { Duration, Names, Resource, Token, ValidationError, withResolved } from '../../core';
88
import { addConstructMetadata } from '../../core/lib/metadata-resource';
9+
import { DetachedConstruct } from '../../core/lib/private/detached-construct';
910
import { propertyInjectable } from '../../core/lib/prop-injectable';
1011

1112
/**
@@ -109,19 +110,14 @@ export class ResponseHeadersPolicy extends Resource implements IResponseHeadersP
109110
}
110111

111112
private static fromManagedResponseHeadersPolicy(managedResponseHeadersPolicyId: string): IResponseHeadersPolicy {
112-
return new class implements IResponseHeadersPolicy {
113-
public get node(): Node {
114-
throw new UnscopedValidationError('The result of fromManagedResponseHeadersPolicy can not be used in this API');
115-
}
116-
117-
public get env(): ResourceEnvironment {
118-
throw new UnscopedValidationError('The result of fromManagedResponseHeadersPolicy can not be used in this API');
119-
}
120-
113+
return new class extends DetachedConstruct implements IResponseHeadersPolicy {
121114
public readonly responseHeadersPolicyId = managedResponseHeadersPolicyId;
122115
public readonly responseHeadersPolicyRef = {
123116
responseHeadersPolicyId: managedResponseHeadersPolicyId,
124117
};
118+
constructor() {
119+
super('The result of fromManagedResponseHeadersPolicy can not be used in this API');
120+
}
125121
};
126122
}
127123

packages/aws-cdk-lib/aws-codepipeline-actions/lib/elastic-beanstalk/deploy-action.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { Construct, Node } from 'constructs';
1+
import { Construct } from 'constructs';
22
import * as codepipeline from '../../../aws-codepipeline';
3-
import { Aws, ResourceEnvironment, UnscopedValidationError } from '../../../core';
3+
import { Aws } from '../../../core';
4+
import { DetachedConstruct } from '../../../core/lib/private/detached-construct';
45
import { Action } from '../action';
56
import { deployArtifactBounds } from '../common';
67

@@ -53,16 +54,13 @@ export class ElasticBeanstalkDeployAction extends Action {
5354
// it doesn't seem we can scope this down further for the codepipeline action.
5455

5556
const policyArn = `arn:${Aws.PARTITION}:iam::aws:policy/AdministratorAccess-AWSElasticBeanstalk`;
56-
options.role.addManagedPolicy({
57-
get node(): Node {
58-
throw new UnscopedValidationError('This object can not be used in this API');
59-
},
60-
get env(): ResourceEnvironment {
61-
throw new UnscopedValidationError('This object can not be used in this API');
62-
},
63-
managedPolicyArn: policyArn,
64-
managedPolicyRef: { policyArn },
65-
});
57+
options.role.addManagedPolicy(new class extends DetachedConstruct {
58+
managedPolicyArn = policyArn;
59+
managedPolicyRef = { policyArn };
60+
constructor() {
61+
super('This object can not be used in this API');
62+
}
63+
}());
6664

6765
// the Action's Role needs to read from the Bucket to get artifacts
6866
options.bucket.grantRead(options.role);

packages/aws-cdk-lib/aws-iam/lib/managed-policy.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Construct, Node } from 'constructs';
1+
import { Construct } from 'constructs';
22
import {
33
CfnManagedPolicy,
44
IGroupRef,
@@ -13,9 +13,10 @@ import { AddToPrincipalPolicyResult, ArnPrincipal, IGrantable, IPrincipal, Princ
1313
import { undefinedIfEmpty } from './private/util';
1414
import { IRole } from './role';
1515
import { IUser } from './user';
16-
import { Arn, ArnFormat, Aws, Resource, ResourceEnvironment, Stack, UnscopedValidationError, ValidationError, Lazy } from '../../core';
16+
import { Arn, ArnFormat, Aws, Resource, Stack, ValidationError, Lazy } from '../../core';
1717
import { getCustomizeRolesConfig, PolicySynthesizer } from '../../core/lib/helpers-internal';
1818
import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource';
19+
import { DetachedConstruct } from '../../core/lib/private/detached-construct';
1920
import { propertyInjectable } from '../../core/lib/prop-injectable';
2021

2122
/**
@@ -179,7 +180,7 @@ export class ManagedPolicy extends Resource implements IManagedPolicy, IGrantabl
179180
* prefix when constructing this object.
180181
*/
181182
public static fromAwsManagedPolicyName(managedPolicyName: string): IManagedPolicy {
182-
class AwsManagedPolicy implements IManagedPolicy {
183+
class AwsManagedPolicy extends DetachedConstruct implements IManagedPolicy {
183184
public readonly managedPolicyArn = Arn.format({
184185
partition: Aws.PARTITION,
185186
service: 'iam',
@@ -188,17 +189,14 @@ export class ManagedPolicy extends Resource implements IManagedPolicy, IGrantabl
188189
resource: 'policy',
189190
resourceName: managedPolicyName,
190191
});
192+
constructor() {
193+
super('The result of fromAwsManagedPolicyName can not be used in this API');
194+
}
191195
public get managedPolicyRef(): ManagedPolicyReference {
192196
return {
193197
policyArn: this.managedPolicyArn,
194198
};
195199
}
196-
public get node(): Node {
197-
throw new UnscopedValidationError('The result of fromAwsManagedPolicyName can not be used in this API');
198-
}
199-
public get env(): ResourceEnvironment {
200-
throw new UnscopedValidationError('The result of fromAwsManagedPolicyName can not be used in this API');
201-
}
202200
}
203201
return new AwsManagedPolicy();
204202
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Construct, IConstruct } from 'constructs';
2+
import type { ResourceEnvironment } from '../environment';
3+
import { UnscopedValidationError } from '../errors';
4+
5+
const CONSTRUCT_SYM = Symbol.for('constructs.Construct');
6+
7+
/**
8+
* Base class for detached constructs that throw UnscopedValidationError
9+
* when accessing node, env, or with() methods.
10+
*
11+
* This is used by legacy APIs like ManagedPolicy.fromAwsManagedPolicyName() and
12+
* CloudFront policy imports that return construct-like objects without requiring
13+
* a scope parameter. These APIs predate modern CDK patterns and cannot be changed
14+
* without breaking existing customer code.
15+
*
16+
* DO NOT USE for new code. New APIs should require a scope parameter.
17+
*
18+
* @internal
19+
*/
20+
export abstract class DetachedConstruct extends Construct implements IConstruct {
21+
private readonly errorMessage: string;
22+
23+
constructor(errorMessage: string) {
24+
super(null as any, undefined as any);
25+
26+
this.errorMessage = errorMessage;
27+
28+
// Use Object.defineProperty to override 'node' property instead of a getter
29+
// to avoid TS2611 error (property vs accessor conflict with base class)
30+
Object.defineProperty(this, 'node', {
31+
get() { throw new UnscopedValidationError(errorMessage); },
32+
});
33+
34+
// Despite extending Construct, DetachedConstruct doesn't work like one.
35+
// So we try to not pretend that this is a construct as much as possible.
36+
Object.defineProperty(this, CONSTRUCT_SYM, {
37+
value: false,
38+
enumerable: false,
39+
writable: false,
40+
});
41+
}
42+
43+
public get env(): ResourceEnvironment {
44+
throw new UnscopedValidationError(this.errorMessage);
45+
}
46+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Construct } from 'constructs';
2+
import { UnscopedValidationError } from '../../lib/errors';
3+
import { DetachedConstruct } from '../../lib/private/detached-construct';
4+
5+
class TestDetachedConstruct extends DetachedConstruct {
6+
constructor(message: string) {
7+
super(message);
8+
}
9+
}
10+
11+
describe('DetachedConstruct', () => {
12+
test('throws UnscopedValidationError when accessing node', () => {
13+
const construct = new TestDetachedConstruct('test error message');
14+
15+
expect(() => construct.node).toThrow(UnscopedValidationError);
16+
expect(() => construct.node).toThrow('test error message');
17+
});
18+
19+
test('throws UnscopedValidationError when accessing env', () => {
20+
const construct = new TestDetachedConstruct('test error message');
21+
22+
expect(() => construct.env).toThrow(UnscopedValidationError);
23+
expect(() => construct.env).toThrow('test error message');
24+
});
25+
26+
test('returns false for Construct.isConstruct', () => {
27+
const construct = new TestDetachedConstruct('test error message');
28+
29+
expect(Construct.isConstruct(construct)).toBe(false);
30+
});
31+
});

0 commit comments

Comments
 (0)