@inizioevoke/aws-cdk-helpers 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +12 -0
- package/constructs/certificatemanager.ts +29 -0
- package/constructs/cloudfront.ts +124 -0
- package/constructs/codebuild.ts +102 -0
- package/constructs/codepipeline.ts +86 -0
- package/constructs/route53.ts +34 -0
- package/constructs/s3.ts +26 -0
- package/constructs/waf.ts +113 -0
- package/lib/types.d.ts +12 -0
- package/lib/utils.ts +23 -0
- package/package.json +13 -0
- package/readme.md +80 -0
- package/samples/app.ts +69 -0
- package/samples/codebuild/buildspec.yml +24 -0
- package/stacks/web-static-serverless/codepipeline-stack.ts +42 -0
- package/stacks/web-static-serverless/web-global-stack.ts +119 -0
- package/stages/str8r-clm-stage.ts +55 -0
- package/stages/web-static-serverless-stage.ts +55 -0
- package/templates/cffn-default-doc-basic-auth.js +148 -0
- package/templates/cffn-default-doc.js +125 -0
- package/tsconfig.json +29 -0
package/.editorconfig
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { RemovalPolicy } from "aws-cdk-lib";
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
import { Certificate, CertificateValidation } from "aws-cdk-lib/aws-certificatemanager";
|
|
4
|
+
import { IHostedZone } from "aws-cdk-lib/aws-route53";
|
|
5
|
+
import { tagResource } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
export interface SSLCertificateProps {
|
|
8
|
+
hostedZone: IHostedZone;
|
|
9
|
+
domainName: string;
|
|
10
|
+
destroy?: boolean;
|
|
11
|
+
tags?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class SSLCertificate extends Construct {
|
|
15
|
+
public readonly certificate: Certificate;
|
|
16
|
+
|
|
17
|
+
constructor(scope: Construct, id: string, props: SSLCertificateProps) {
|
|
18
|
+
super(scope, id);
|
|
19
|
+
|
|
20
|
+
this.certificate = new Certificate(this, `Certificate-${props.domainName}`, {
|
|
21
|
+
domainName: props.domainName,
|
|
22
|
+
validation: CertificateValidation.fromDns(props.hostedZone),
|
|
23
|
+
});
|
|
24
|
+
tagResource(this.certificate, props.tags);
|
|
25
|
+
if (typeof props.destroy == 'boolean') {
|
|
26
|
+
this.certificate.applyRemovalPolicy(props.destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { Buffer } from 'node:buffer';
|
|
4
|
+
import { RemovalPolicy } from "aws-cdk-lib";
|
|
5
|
+
import { Construct } from 'constructs';
|
|
6
|
+
import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager';
|
|
7
|
+
import { BehaviorOptions, Distribution, Function, FunctionCode, FunctionEventType, FunctionRuntime, IOrigin, PriceClass } from 'aws-cdk-lib/aws-cloudfront';
|
|
8
|
+
import { tagResource } from '../lib/utils';
|
|
9
|
+
import type { IResourceProps } from '../lib/types';
|
|
10
|
+
|
|
11
|
+
export interface CloudFrontDistributionProps extends IResourceProps {
|
|
12
|
+
domainName: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
defaultBehavior: BehaviorOptions;
|
|
15
|
+
additionalBehaviors?: Record<string, BehaviorOptions>;
|
|
16
|
+
priceClass?: 'PRICE_CLASS_ALL' | 'PRICE_CLASS_100' | 'PRICE_CLASS_200' | 'NOT_PROD' | 'PROD';
|
|
17
|
+
certificate: ICertificate;
|
|
18
|
+
webAclId?: string;
|
|
19
|
+
}
|
|
20
|
+
export class CloudFrontDistribution extends Construct {
|
|
21
|
+
public readonly distribution: Distribution;
|
|
22
|
+
|
|
23
|
+
constructor(scope: Construct, id: string, props: CloudFrontDistributionProps) {
|
|
24
|
+
super(scope, id);
|
|
25
|
+
|
|
26
|
+
let priceClass = PriceClass.PRICE_CLASS_ALL;
|
|
27
|
+
switch (props.priceClass) {
|
|
28
|
+
case 'NOT_PROD':
|
|
29
|
+
case 'PRICE_CLASS_100':
|
|
30
|
+
priceClass = PriceClass.PRICE_CLASS_100;
|
|
31
|
+
break;
|
|
32
|
+
case 'PRICE_CLASS_200':
|
|
33
|
+
priceClass = PriceClass.PRICE_CLASS_200;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.distribution = new Distribution(this, `${id}|CloudFrontDistribution`, {
|
|
38
|
+
comment: props.description,
|
|
39
|
+
defaultBehavior: props.defaultBehavior,
|
|
40
|
+
additionalBehaviors: props.additionalBehaviors,
|
|
41
|
+
domainNames: [props.domainName],
|
|
42
|
+
certificate: props.certificate,
|
|
43
|
+
priceClass,
|
|
44
|
+
webAclId: props.webAclId
|
|
45
|
+
});
|
|
46
|
+
tagResource(this.distribution, props.tags);
|
|
47
|
+
if (typeof props.destroy == 'boolean') {
|
|
48
|
+
this.distribution.applyRemovalPolicy(props.destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type CloudFrontFunctionRedirectPaths = Record<string, string | [string, 301|302]>;
|
|
54
|
+
|
|
55
|
+
export interface CloudFrontFunctionRedirects {
|
|
56
|
+
trailingSlash?: boolean;
|
|
57
|
+
defaultDocument?: string;
|
|
58
|
+
redirectDefaultDocument?: boolean;
|
|
59
|
+
paths?: CloudFrontFunctionRedirectPaths;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DefaultDocFunctionProps extends IResourceProps {
|
|
63
|
+
functionName?: string;
|
|
64
|
+
defaultDocument?: string;
|
|
65
|
+
redirects?: CloudFrontFunctionRedirects;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface BasicAuthDefaultDocFunctionProps extends DefaultDocFunctionProps {
|
|
69
|
+
user: string;
|
|
70
|
+
password: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface CloudFrontFunctionProps extends IResourceProps {
|
|
74
|
+
code: string;
|
|
75
|
+
functionName?: string;
|
|
76
|
+
runtime?: 'JS_1_0' | 'JS_2_0';
|
|
77
|
+
}
|
|
78
|
+
export class CloudFrontFunction extends Construct {
|
|
79
|
+
public readonly fn: Function;
|
|
80
|
+
|
|
81
|
+
constructor(scope: Construct, id: string, props: CloudFrontFunctionProps) {
|
|
82
|
+
super(scope, id);
|
|
83
|
+
|
|
84
|
+
this.fn = new Function(this, 'CloudFrontFunction', {
|
|
85
|
+
functionName: props.functionName,
|
|
86
|
+
runtime: FunctionRuntime[props.runtime ?? 'JS_2_0'],
|
|
87
|
+
code: FunctionCode.fromInline(props.code)
|
|
88
|
+
});
|
|
89
|
+
tagResource(this.fn, props.tags);
|
|
90
|
+
|
|
91
|
+
if (typeof props.destroy == 'boolean') {
|
|
92
|
+
this.fn.applyRemovalPolicy(props.destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static createDefaultDoc(scope: Construct, id: string, props: DefaultDocFunctionProps) {
|
|
97
|
+
const code = readFileSync(resolve(__dirname, '../templates/cffn-default-doc.js'), 'utf8')
|
|
98
|
+
.replace('__DEFAULT_DOCUMENT__', props.defaultDocument ?? 'index.html')
|
|
99
|
+
.replace('__REDIRECTS__', props.redirects?.paths ? JSON.stringify(props.redirects?.paths ) : 'null')
|
|
100
|
+
.replace('__TRAILING_SLASH__', props.redirects?.trailingSlash?.toString() ?? 'null')
|
|
101
|
+
.replace('__REDIR_DEF_DOC__', props.redirects?.redirectDefaultDocument?.toString() ?? 'false');
|
|
102
|
+
|
|
103
|
+
return new CloudFrontFunction(scope, 'DefaultDocFunction', {
|
|
104
|
+
functionName: props.functionName,
|
|
105
|
+
runtime: 'JS_2_0',
|
|
106
|
+
code
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
static createBasicAuthDefaultDoc(scope: Construct, id: string, props: BasicAuthDefaultDocFunctionProps) {
|
|
111
|
+
const code = readFileSync(resolve(__dirname, '../templates/cffn-default-doc-basic-auth.js'), 'utf8')
|
|
112
|
+
.replace('__DEFAULT_DOCUMENT__', props.defaultDocument ?? 'index.html')
|
|
113
|
+
.replace('__CREDENTIALS__', Buffer.from(`${props.user}:${props.password}`).toString('base64'))
|
|
114
|
+
.replace('__REDIRECTS__', props.redirects?.paths ? JSON.stringify(props.redirects?.paths ) : 'null')
|
|
115
|
+
.replace('__TRAILING_SLASH__', props.redirects?.trailingSlash?.toString() ?? 'null')
|
|
116
|
+
.replace('__REDIR_DEF_DOC__', props.redirects?.redirectDefaultDocument?.toString() ?? 'false');
|
|
117
|
+
|
|
118
|
+
return new CloudFrontFunction(scope, 'BasicAuthDefaultDocFunction', {
|
|
119
|
+
functionName: props.functionName,
|
|
120
|
+
runtime: 'JS_2_0',
|
|
121
|
+
code
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { RemovalPolicy } from "aws-cdk-lib";
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
import { BuildEnvironmentVariableType, BuildSpec, ComputeType, IBuildImage, LinuxBuildImage, PipelineProject } from 'aws-cdk-lib/aws-codebuild';
|
|
4
|
+
import { Repository } from "aws-cdk-lib/aws-ecr";
|
|
5
|
+
import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
|
|
6
|
+
import { Bucket, IBucket } from 'aws-cdk-lib/aws-s3';
|
|
7
|
+
|
|
8
|
+
import { tagResource } from "../lib/utils";
|
|
9
|
+
|
|
10
|
+
export type EnvironmentVariables = Record<string, { value: any, type?: BuildEnvironmentVariableType }>;
|
|
11
|
+
|
|
12
|
+
export interface EcrBuildImage {
|
|
13
|
+
repository: string;
|
|
14
|
+
tag: string;
|
|
15
|
+
}
|
|
16
|
+
export interface CodeBuildPipelineProjectProps {
|
|
17
|
+
projectName?: string;
|
|
18
|
+
buildSpec?: string | object;
|
|
19
|
+
buildImage?: ('STANDARD_5_0' | 'STANDARD_6_0' | 'STANDARD_7_0') | IBuildImage;
|
|
20
|
+
ecrBuildImage?: EcrBuildImage,
|
|
21
|
+
computeType?: ('SMALL' | 'MEDIUM' | 'LARGE' | 'X_LARGE' | 'X2_LARGE') | string;
|
|
22
|
+
environmentVariables?: EnvironmentVariables;
|
|
23
|
+
cloudFrontDistributionArn?: string;
|
|
24
|
+
s3BucketName?: string;
|
|
25
|
+
destroy?: boolean;
|
|
26
|
+
tags?: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class CodeBuildPipelineProject extends Construct {
|
|
30
|
+
public readonly project: PipelineProject;
|
|
31
|
+
|
|
32
|
+
constructor(scope: Construct, id: string, props: CodeBuildPipelineProjectProps) {
|
|
33
|
+
super(scope, id);
|
|
34
|
+
|
|
35
|
+
if (!props.environmentVariables) {
|
|
36
|
+
props.environmentVariables = {};
|
|
37
|
+
}
|
|
38
|
+
if (props.cloudFrontDistributionArn && !props.environmentVariables.CLOUDFRONT_DISTRIBUTION_ID) {
|
|
39
|
+
props.environmentVariables.CLOUDFRONT_DISTRIBUTION_ID = {
|
|
40
|
+
value: props.cloudFrontDistributionArn.split('/').pop()
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (props.s3BucketName && !props.environmentVariables.S3_BUCKET_NAME) {
|
|
45
|
+
props.environmentVariables.S3_BUCKET_NAME = {
|
|
46
|
+
value: props.s3BucketName
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let buildImage: IBuildImage | undefined = undefined;
|
|
51
|
+
if (props.ecrBuildImage) {
|
|
52
|
+
const repository = props.ecrBuildImage.repository.startsWith('arn:aws') ?
|
|
53
|
+
Repository.fromRepositoryArn(this, 'EcrRepository', props.ecrBuildImage.repository) :
|
|
54
|
+
Repository.fromRepositoryName(this, 'EcrRepository', props.ecrBuildImage.repository);
|
|
55
|
+
buildImage = LinuxBuildImage.fromEcrRepository(repository, props.ecrBuildImage.tag);
|
|
56
|
+
} else if (props.buildImage) {
|
|
57
|
+
if (typeof props.buildImage == 'string') {
|
|
58
|
+
buildImage = LinuxBuildImage[props.buildImage ?? 'STANDARD_7_0']
|
|
59
|
+
} else {
|
|
60
|
+
buildImage = props.buildImage as IBuildImage;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.project = new PipelineProject(this, 'CodeBuildPipelineProjectProps', {
|
|
65
|
+
projectName: props.projectName,
|
|
66
|
+
buildSpec: props.buildSpec ? typeof props.buildSpec == 'object' ? BuildSpec.fromObjectToYaml(props.buildSpec) : BuildSpec.fromSourceFilename(props.buildSpec ?? 'buildspec.yml') : undefined,
|
|
67
|
+
environment: {
|
|
68
|
+
buildImage: buildImage ?? LinuxBuildImage.STANDARD_7_0,
|
|
69
|
+
computeType: ComputeType[props.computeType as (keyof typeof ComputeType | undefined) ?? 'SMALL'],
|
|
70
|
+
environmentVariables: props.environmentVariables
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
tagResource(this.project, props.tags);
|
|
74
|
+
if (typeof props.destroy == 'boolean') {
|
|
75
|
+
this.project.applyRemovalPolicy(props.destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Add CloudWatch log permissions
|
|
79
|
+
this.project.addToRolePolicy(new PolicyStatement({
|
|
80
|
+
actions: [
|
|
81
|
+
'logs:CreateLogGroup',
|
|
82
|
+
'logs:CreateLogStream',
|
|
83
|
+
'logs:PutLogEvents'
|
|
84
|
+
],
|
|
85
|
+
resources: [this.project.projectArn]
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
// Allow the project to create an invalidation
|
|
89
|
+
if (props.cloudFrontDistributionArn) {
|
|
90
|
+
this.project.addToRolePolicy(new PolicyStatement({
|
|
91
|
+
actions: ['cloudfront:CreateInvalidation'],
|
|
92
|
+
resources: [props.cloudFrontDistributionArn]
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Allow the project to sync files to S3
|
|
97
|
+
if (props.s3BucketName) {
|
|
98
|
+
const s3Bucket = Bucket.fromBucketName(this, 'S3BucketByName', props.s3BucketName);
|
|
99
|
+
s3Bucket.grantReadWrite(this.project);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { RemovalPolicy, PhysicalName } from "aws-cdk-lib";
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
import { IProject } from "aws-cdk-lib/aws-codebuild";
|
|
4
|
+
import { Artifact, Pipeline } from "aws-cdk-lib/aws-codepipeline";
|
|
5
|
+
import { CodeBuildAction, CodeStarConnectionsSourceAction } from "aws-cdk-lib/aws-codepipeline-actions";
|
|
6
|
+
import { Bucket } from 'aws-cdk-lib/aws-s3';
|
|
7
|
+
|
|
8
|
+
import type { EnvironmentVariables } from './codebuild';
|
|
9
|
+
import { tagResource } from "../lib/utils";
|
|
10
|
+
|
|
11
|
+
export interface AddSourceStageParams {
|
|
12
|
+
stageName?: string;
|
|
13
|
+
codestarConnectionArn: string;
|
|
14
|
+
repoOwner: string;
|
|
15
|
+
repo: string;
|
|
16
|
+
branch: string;
|
|
17
|
+
}
|
|
18
|
+
export interface AddBuildStageParams {
|
|
19
|
+
stageName?: string;
|
|
20
|
+
codeBuildProject: IProject;
|
|
21
|
+
sourceArtifact: Artifact;
|
|
22
|
+
environmentVariables?: EnvironmentVariables;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CodePipelineProjectProps {
|
|
26
|
+
pipelineName?: string;
|
|
27
|
+
destroy?: boolean;
|
|
28
|
+
tags?: Record<string, string>;
|
|
29
|
+
}
|
|
30
|
+
export class CodePipelineProject extends Construct {
|
|
31
|
+
public readonly s3Bucket: Bucket;
|
|
32
|
+
public readonly pipeline: Pipeline;
|
|
33
|
+
|
|
34
|
+
constructor(scope: Construct, id: string, props: CodePipelineProjectProps) {
|
|
35
|
+
super(scope, id);
|
|
36
|
+
|
|
37
|
+
this.s3Bucket = new Bucket(this, 'CodePipelineArtifactBucket', {
|
|
38
|
+
bucketName: PhysicalName.GENERATE_IF_NEEDED,
|
|
39
|
+
removalPolicy: props.destroy !== undefined ? props.destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN : undefined,
|
|
40
|
+
autoDeleteObjects: props.destroy === true ? true : undefined
|
|
41
|
+
});
|
|
42
|
+
tagResource(this.s3Bucket, props.tags);
|
|
43
|
+
|
|
44
|
+
this.pipeline = new Pipeline(this, 'CodePipelineProject', {
|
|
45
|
+
pipelineName: props.pipelineName,
|
|
46
|
+
artifactBucket: this.s3Bucket
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
tagResource(this.pipeline, props.tags);
|
|
50
|
+
if (typeof props.destroy == 'boolean') {
|
|
51
|
+
this.pipeline.applyRemovalPolicy(props.destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
addSourceStage(params: AddSourceStageParams): Artifact {
|
|
56
|
+
const artifact = new Artifact();
|
|
57
|
+
this.pipeline.addStage({
|
|
58
|
+
stageName: params.stageName ?? 'Source',
|
|
59
|
+
actions: [
|
|
60
|
+
new CodeStarConnectionsSourceAction({
|
|
61
|
+
actionName: params.stageName ?? 'Source',
|
|
62
|
+
output: artifact,
|
|
63
|
+
connectionArn: params.codestarConnectionArn,
|
|
64
|
+
owner: params.repoOwner,
|
|
65
|
+
repo: params.repo,
|
|
66
|
+
branch: params.branch
|
|
67
|
+
})
|
|
68
|
+
]
|
|
69
|
+
});
|
|
70
|
+
return artifact;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
addBuildStage(params: AddBuildStageParams) {
|
|
74
|
+
this.pipeline.addStage({
|
|
75
|
+
stageName: params.stageName ?? 'Build',
|
|
76
|
+
actions: [
|
|
77
|
+
new CodeBuildAction({
|
|
78
|
+
actionName: params.stageName ?? 'Build',
|
|
79
|
+
project: params.codeBuildProject,
|
|
80
|
+
input: params.sourceArtifact,
|
|
81
|
+
environmentVariables: params.environmentVariables
|
|
82
|
+
})
|
|
83
|
+
]
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { IDistribution } from "aws-cdk-lib/aws-cloudfront";
|
|
2
|
+
import { ARecord, AaaaRecord, HostedZone, IHostedZone, RecordTarget } from "aws-cdk-lib/aws-route53";
|
|
3
|
+
import { CloudFrontTarget } from "aws-cdk-lib/aws-route53-targets";
|
|
4
|
+
import { Construct } from "constructs";
|
|
5
|
+
|
|
6
|
+
export interface CloudFrontAliasProps {
|
|
7
|
+
hostedZone: IHostedZone;
|
|
8
|
+
domainName: string;
|
|
9
|
+
distribution: IDistribution;
|
|
10
|
+
}
|
|
11
|
+
export class CloudFrontAlias extends Construct {
|
|
12
|
+
public readonly aRecord: ARecord;
|
|
13
|
+
public readonly aaaaRecord: AaaaRecord;
|
|
14
|
+
|
|
15
|
+
constructor(scope: Construct, id: string, props: CloudFrontAliasProps) {
|
|
16
|
+
super(scope, id);
|
|
17
|
+
|
|
18
|
+
this.aRecord = new ARecord(this, `ARecord-${props.domainName}`, {
|
|
19
|
+
zone: props.hostedZone,
|
|
20
|
+
target: RecordTarget.fromAlias(new CloudFrontTarget(props.distribution)),
|
|
21
|
+
recordName: props.domainName
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
this.aaaaRecord = new AaaaRecord(this, `AaaaRecord-${props.domainName}`, {
|
|
25
|
+
zone: props.hostedZone,
|
|
26
|
+
target: RecordTarget.fromAlias(new CloudFrontTarget(props.distribution)),
|
|
27
|
+
recordName: props.domainName
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getHostedZone(scope: Construct, id: string, domainName: string) {
|
|
33
|
+
return HostedZone.fromLookup(scope, `getHostedZone-${domainName}`, { domainName });
|
|
34
|
+
}
|
package/constructs/s3.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { RemovalPolicy } from "aws-cdk-lib";
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
import { Bucket } from 'aws-cdk-lib/aws-s3';
|
|
4
|
+
import { tagResource } from '../lib/utils';
|
|
5
|
+
|
|
6
|
+
export interface S3BucketProps {
|
|
7
|
+
/** The name of the S3 bucket */
|
|
8
|
+
bucketName?: string;
|
|
9
|
+
destroy?: boolean;
|
|
10
|
+
tags?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class S3Bucket extends Construct {
|
|
14
|
+
public readonly bucket: Bucket;
|
|
15
|
+
|
|
16
|
+
constructor(scope: Construct, id: string, props: S3BucketProps) {
|
|
17
|
+
super(scope, id);
|
|
18
|
+
|
|
19
|
+
this.bucket = new Bucket(this, 'S3Bucket', {
|
|
20
|
+
bucketName: props.bucketName,
|
|
21
|
+
removalPolicy: props.destroy !== undefined ? props.destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN : undefined,
|
|
22
|
+
autoDeleteObjects: props.destroy === true ? true : undefined
|
|
23
|
+
});
|
|
24
|
+
tagResource(this.bucket, props.tags);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { RemovalPolicy } from "aws-cdk-lib";
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
import { CfnWebACL } from 'aws-cdk-lib/aws-wafv2';
|
|
4
|
+
import { cleanResourceName, tagResource } from '../lib/utils';
|
|
5
|
+
import type { IResourceProps } from '../lib/types';
|
|
6
|
+
|
|
7
|
+
export type WebAclManagedRule =
|
|
8
|
+
'AWSManagedRulesAmazonIpReputationList' |
|
|
9
|
+
'AWSManagedRulesAnonymousIpList' |
|
|
10
|
+
'AWSManagedRulesCommonRuleSet' |
|
|
11
|
+
'AWSManagedRulesKnownBadInputsRuleSet' |
|
|
12
|
+
'AWSManagedRulesSQLiRuleSet';
|
|
13
|
+
|
|
14
|
+
export interface CloudFrontWebAclProps extends IResourceProps {
|
|
15
|
+
name: string;
|
|
16
|
+
managedRules?: (WebAclManagedRule & string)[];
|
|
17
|
+
}
|
|
18
|
+
export class CloudFrontWebAcl extends Construct {
|
|
19
|
+
public readonly acl: CfnWebACL;
|
|
20
|
+
|
|
21
|
+
constructor(scope: Construct, id: string, props: CloudFrontWebAclProps) {
|
|
22
|
+
super(scope, id);
|
|
23
|
+
|
|
24
|
+
props.name = cleanResourceName(props.name);
|
|
25
|
+
|
|
26
|
+
if (!props.managedRules) {
|
|
27
|
+
props.managedRules = [
|
|
28
|
+
'AWSManagedRulesAmazonIpReputationList',
|
|
29
|
+
'AWSManagedRulesAnonymousIpList',
|
|
30
|
+
'AWSManagedRulesCommonRuleSet',
|
|
31
|
+
'AWSManagedRulesKnownBadInputsRuleSet'
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.acl = new CfnWebACL(this, 'CloudFrontWebAcl', {
|
|
36
|
+
scope: 'CLOUDFRONT',
|
|
37
|
+
name: props.name,
|
|
38
|
+
defaultAction: { allow: {} },
|
|
39
|
+
visibilityConfig: {
|
|
40
|
+
cloudWatchMetricsEnabled: true,
|
|
41
|
+
sampledRequestsEnabled: true,
|
|
42
|
+
metricName: `${props.name}-metrics`
|
|
43
|
+
},
|
|
44
|
+
rules: transformManagedRules(props.managedRules)
|
|
45
|
+
});
|
|
46
|
+
tagResource(this.acl, props.tags);
|
|
47
|
+
if (typeof props.destroy == 'boolean') {
|
|
48
|
+
this.acl.applyRemovalPolicy(props.destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ApiGatewayWebAclProps extends IResourceProps {
|
|
54
|
+
name: string;
|
|
55
|
+
aclScope?: 'CLOUDFRONT' | 'REGIONAL';
|
|
56
|
+
managedRules?: (WebAclManagedRule & string)[];
|
|
57
|
+
}
|
|
58
|
+
export class ApiGatewayWebAcl extends Construct {
|
|
59
|
+
public readonly acl: CfnWebACL;
|
|
60
|
+
constructor(scope: Construct, id: string, props: ApiGatewayWebAclProps) {
|
|
61
|
+
super(scope, id);
|
|
62
|
+
|
|
63
|
+
props.name = cleanResourceName(props.name);
|
|
64
|
+
|
|
65
|
+
if (!props.managedRules) {
|
|
66
|
+
props.managedRules = [
|
|
67
|
+
'AWSManagedRulesAmazonIpReputationList',
|
|
68
|
+
'AWSManagedRulesAnonymousIpList',
|
|
69
|
+
'AWSManagedRulesCommonRuleSet',
|
|
70
|
+
'AWSManagedRulesKnownBadInputsRuleSet',
|
|
71
|
+
'AWSManagedRulesSQLiRuleSet'
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.acl = new CfnWebACL(this, 'ApiGatewayWebAcl', {
|
|
76
|
+
scope: props.aclScope ?? 'REGIONAL',
|
|
77
|
+
name: props.name,
|
|
78
|
+
defaultAction: { allow: {} },
|
|
79
|
+
visibilityConfig: {
|
|
80
|
+
cloudWatchMetricsEnabled: true,
|
|
81
|
+
sampledRequestsEnabled: true,
|
|
82
|
+
metricName: `${props.name}-metrics`
|
|
83
|
+
},
|
|
84
|
+
rules: transformManagedRules(props.managedRules)
|
|
85
|
+
});
|
|
86
|
+
tagResource(this.acl, props.tags);
|
|
87
|
+
if (typeof props.destroy == 'boolean') {
|
|
88
|
+
this.acl.applyRemovalPolicy(props.destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
function transformManagedRules(rules: (WebAclManagedRule | string)[]): CfnWebACL.RuleProperty[] {
|
|
95
|
+
return rules.map((ruleName, i) => {
|
|
96
|
+
return {
|
|
97
|
+
name: `AWS-${ruleName}`,
|
|
98
|
+
priority: i,
|
|
99
|
+
statement: {
|
|
100
|
+
managedRuleGroupStatement: {
|
|
101
|
+
name: ruleName,
|
|
102
|
+
vendorName: 'AWS'
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
overrideAction: { none: {} },
|
|
106
|
+
visibilityConfig: {
|
|
107
|
+
cloudWatchMetricsEnabled: true,
|
|
108
|
+
sampledRequestsEnabled: true,
|
|
109
|
+
metricName: `AWS-${ruleName}`
|
|
110
|
+
}
|
|
111
|
+
} as CfnWebACL.RuleProperty;
|
|
112
|
+
});
|
|
113
|
+
}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type DeepPartial<T> = T extends object ? {
|
|
2
|
+
[P in keyof T]?: DeepPartial<T[P]>;
|
|
3
|
+
} : T;
|
|
4
|
+
|
|
5
|
+
export type WithRequiredProperty<Type, Key extends keyof Type> = Omit<Type, Key> & {
|
|
6
|
+
[Property in Key]-?: Type[Property];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export interface IResourceProps {
|
|
10
|
+
destroy?: boolean;
|
|
11
|
+
tags?: Record<string, string>;
|
|
12
|
+
}
|
package/lib/utils.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { IConstruct } from 'constructs';
|
|
2
|
+
import { Tags } from 'aws-cdk-lib/core';
|
|
3
|
+
|
|
4
|
+
export function domainAsId(domain: string) {
|
|
5
|
+
return domain.replace(/\./g, '-');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function cleanResourceName(name: string) {
|
|
9
|
+
return name.replace(/[^0-9a-z_\-]/gi, '');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function tagResource(resource: IConstruct, tags?: Record<string, string>) {
|
|
13
|
+
if (tags) {
|
|
14
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
15
|
+
Tags.of(resource).add(key, value);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function mergeTags(...tags: (Record<string, string> | null | undefined)[]): Record<string, string> {
|
|
21
|
+
const _tags = tags.filter(t => t !== undefined && t !== null) ?? [];
|
|
22
|
+
return Object.assign({}, ..._tags);
|
|
23
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@inizioevoke/aws-cdk-helpers",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"author": "",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"description": "",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@types/node": "^25.3.0",
|
|
9
|
+
"aws-cdk-lib": "^2.238.0",
|
|
10
|
+
"constructs": "^10.5.1",
|
|
11
|
+
"typescript": "^5.9.3"
|
|
12
|
+
}
|
|
13
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @inizioevoke/aws-cdk-helpers
|
|
2
|
+
|
|
3
|
+
## Example App
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import * as cdk from 'aws-cdk-lib/core';
|
|
7
|
+
import { CloudFrontFunctionRedirects } from '@inizioevoke/aws-cdk-helpers/constructs/cloudfront';
|
|
8
|
+
import { WebStaticServerlessStage } from '@inizioevoke/aws-cdk-helpers/stages/web-static-serverless-stage';
|
|
9
|
+
import { AddSourceStageParams } from '@inizioevoke/aws-cdk-helpers/constructs/codepipeline';
|
|
10
|
+
|
|
11
|
+
const app = new cdk.App();
|
|
12
|
+
|
|
13
|
+
const env = {
|
|
14
|
+
account: '000000000000',
|
|
15
|
+
region: 'us-east-1'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const tags: Record<string, string> = {
|
|
19
|
+
'Account': 'Account Name',
|
|
20
|
+
'Brand': 'Brand Name'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const repository: Omit<AddSourceStageParams, 'branch'> = {
|
|
24
|
+
codestarConnectionArn: 'ARN',
|
|
25
|
+
repoOwner: 'evokegroup',
|
|
26
|
+
repo: 'myproject-v1'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const redirects: CloudFrontFunctionRedirects = {
|
|
30
|
+
paths: {
|
|
31
|
+
'/hello': '/world',
|
|
32
|
+
'/redirect': ['/here', 302]
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
new WebStaticServerlessStage(app, 'dev', {
|
|
37
|
+
env,
|
|
38
|
+
tags,
|
|
39
|
+
stageName: 'myproject-v1-dev',
|
|
40
|
+
envType: 'NOT_PROD',
|
|
41
|
+
description: 'My Project DEV',
|
|
42
|
+
hostedZone: 'evodev.net',
|
|
43
|
+
domainName: 'myproject-v1.evodev.net',
|
|
44
|
+
basicAuth: {
|
|
45
|
+
user: 'user',
|
|
46
|
+
password: 'password'
|
|
47
|
+
},
|
|
48
|
+
redirects,
|
|
49
|
+
pipeline: {
|
|
50
|
+
sourceStage: {
|
|
51
|
+
...repository,
|
|
52
|
+
branch: 'develop'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
new WebStaticServerlessStage(app, 'prd', {
|
|
58
|
+
env,
|
|
59
|
+
tags,
|
|
60
|
+
stageName: 'myproject-v1-prd',
|
|
61
|
+
envType: 'PROD',
|
|
62
|
+
description: 'My Project PRD',
|
|
63
|
+
hostedZone: 'myproject.com',
|
|
64
|
+
domainName: 'www.myproject.com',
|
|
65
|
+
attachWebAcl: true,
|
|
66
|
+
redirects,
|
|
67
|
+
pipeline: {
|
|
68
|
+
sourceStage: {
|
|
69
|
+
...repository,
|
|
70
|
+
branch: 'prod'
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
```
|
|
76
|
+
cdk deploy dev/*
|
|
77
|
+
```
|
|
78
|
+
```
|
|
79
|
+
cdk deploy prd/*
|
|
80
|
+
```
|
package/samples/app.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as cdk from 'aws-cdk-lib/core';
|
|
3
|
+
import { CloudFrontFunctionRedirects } from '../constructs/cloudfront'; // @inizioevoke/aws-cdk-helpers
|
|
4
|
+
import { WebStaticServerlessStage } from '../stages/web-static-serverless-stage'; // @inizioevoke/aws-cdk-helpers
|
|
5
|
+
import { AddSourceStageParams } from '../constructs/codepipeline'; // @inizioevoke/aws-cdk-helpers
|
|
6
|
+
|
|
7
|
+
const app = new cdk.App();
|
|
8
|
+
|
|
9
|
+
const env = {
|
|
10
|
+
account: '000000000000',
|
|
11
|
+
region: 'us-east-1'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const tags: Record<string, string> = {
|
|
15
|
+
'Account': 'Account Name',
|
|
16
|
+
'Brand': 'Brand Name'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const repository: Omit<AddSourceStageParams, 'branch'> = {
|
|
20
|
+
codestarConnectionArn: 'ARN',
|
|
21
|
+
repoOwner: 'evokegroup',
|
|
22
|
+
repo: 'myproject-v1'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const redirects: CloudFrontFunctionRedirects = {
|
|
26
|
+
paths: {
|
|
27
|
+
'/hello': '/world',
|
|
28
|
+
'/redirect': ['/here', 302]
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
new WebStaticServerlessStage(app, 'dev', {
|
|
33
|
+
env,
|
|
34
|
+
tags,
|
|
35
|
+
stageName: 'myproject-v1-dev',
|
|
36
|
+
envType: 'NOT_PROD',
|
|
37
|
+
description: 'My Project DEV',
|
|
38
|
+
hostedZone: 'evodev.net',
|
|
39
|
+
domainName: 'myproject-v1.evodev.net',
|
|
40
|
+
basicAuth: {
|
|
41
|
+
user: 'user',
|
|
42
|
+
password: 'password'
|
|
43
|
+
},
|
|
44
|
+
redirects,
|
|
45
|
+
pipeline: {
|
|
46
|
+
sourceStage: {
|
|
47
|
+
...repository,
|
|
48
|
+
branch: 'develop'
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
new WebStaticServerlessStage(app, 'prd', {
|
|
54
|
+
env,
|
|
55
|
+
tags,
|
|
56
|
+
stageName: 'myproject-v1-prd',
|
|
57
|
+
envType: 'PROD',
|
|
58
|
+
description: 'My Project PRD',
|
|
59
|
+
hostedZone: 'myproject.com',
|
|
60
|
+
domainName: 'www.myproject.com',
|
|
61
|
+
attachWebAcl: true,
|
|
62
|
+
redirects,
|
|
63
|
+
pipeline: {
|
|
64
|
+
sourceStage: {
|
|
65
|
+
...repository,
|
|
66
|
+
branch: 'prod'
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
version: 0.2
|
|
2
|
+
|
|
3
|
+
env:
|
|
4
|
+
variables:
|
|
5
|
+
# Environment variables
|
|
6
|
+
S3_BUCKET_NAME:
|
|
7
|
+
CLOUDFRONT_DISTRIBUTION_ID:
|
|
8
|
+
|
|
9
|
+
phases:
|
|
10
|
+
install:
|
|
11
|
+
runtime-versions:
|
|
12
|
+
nodejs: 24
|
|
13
|
+
commands:
|
|
14
|
+
- npm install
|
|
15
|
+
build:
|
|
16
|
+
commands:
|
|
17
|
+
- npm run build
|
|
18
|
+
post_build:
|
|
19
|
+
commands:
|
|
20
|
+
- '[ ${CODEBUILD_BUILD_SUCCEEDING:-0} -eq 1 ] || exit 1'
|
|
21
|
+
- echo "Syncing to S3"
|
|
22
|
+
- aws s3 sync dist s3://${S3_BUCKET_NAME}/ --delete
|
|
23
|
+
- echo "Invalidating CloudFront"
|
|
24
|
+
- aws cloudfront create-invalidation --distribution-id ${CLOUDFRONT_DISTRIBUTION_ID} --paths "/*"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Stack, StackProps } from 'aws-cdk-lib/core';
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
import { CodeBuildPipelineProject, CodeBuildPipelineProjectProps } from '../../constructs/codebuild';
|
|
4
|
+
import { CodePipelineProject, AddSourceStageParams, AddBuildStageParams } from '../../constructs/codepipeline';
|
|
5
|
+
|
|
6
|
+
export interface CodePipelineStackProps {
|
|
7
|
+
pipelineName?: string;
|
|
8
|
+
sourceStage: AddSourceStageParams;
|
|
9
|
+
buildStage?: Pick<AddBuildStageParams, 'stageName' | 'environmentVariables'>;
|
|
10
|
+
codeBuildProject?: Omit<CodeBuildPipelineProjectProps, 'destroy' | 'tags'>,
|
|
11
|
+
destroy?: boolean;
|
|
12
|
+
tags?: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
export class CodePipelineStack extends Stack {
|
|
15
|
+
public readonly codeBuildPipelineProject: CodeBuildPipelineProject;
|
|
16
|
+
public readonly codePipelineProject: CodePipelineProject;
|
|
17
|
+
|
|
18
|
+
constructor(scope: Construct, id: string, props: CodePipelineStackProps & StackProps) {
|
|
19
|
+
super(scope, id, props);
|
|
20
|
+
|
|
21
|
+
this.codeBuildPipelineProject = new CodeBuildPipelineProject(this, 'CodeBuildPipelineProject', {
|
|
22
|
+
...(props.codeBuildProject ?? {}),
|
|
23
|
+
destroy: props.destroy,
|
|
24
|
+
tags: props.tags
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
this.codePipelineProject = new CodePipelineProject(this, 'CodePipelineProject', {
|
|
28
|
+
pipelineName: props.pipelineName,
|
|
29
|
+
destroy: props.destroy,
|
|
30
|
+
tags: props.tags
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const sourceArtifact = this.codePipelineProject.addSourceStage(props.sourceStage);
|
|
34
|
+
|
|
35
|
+
this.codePipelineProject.addBuildStage({
|
|
36
|
+
stageName: props.buildStage?.stageName,
|
|
37
|
+
codeBuildProject: this.codeBuildPipelineProject.project,
|
|
38
|
+
sourceArtifact: sourceArtifact,
|
|
39
|
+
environmentVariables: props.buildStage?.environmentVariables
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Stack, StackProps, PhysicalName } from 'aws-cdk-lib/core';
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
|
|
4
|
+
import { Certificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
5
|
+
import { BehaviorOptions, Distribution, Function, FunctionEventType } from 'aws-cdk-lib/aws-cloudfront';
|
|
6
|
+
import { ARecord, AaaaRecord } from "aws-cdk-lib/aws-route53";
|
|
7
|
+
import { Bucket } from "aws-cdk-lib/aws-s3";
|
|
8
|
+
import { CfnWebACL } from 'aws-cdk-lib/aws-wafv2';
|
|
9
|
+
|
|
10
|
+
import { S3BucketOrigin } from 'aws-cdk-lib/aws-cloudfront-origins';
|
|
11
|
+
import { SSLCertificate } from '../../constructs/certificatemanager';
|
|
12
|
+
import { CloudFrontFunctionRedirects, CloudFrontFunction, CloudFrontDistribution } from '../../constructs/cloudfront';
|
|
13
|
+
import { getHostedZone, CloudFrontAlias } from '../../constructs/route53';
|
|
14
|
+
import { S3Bucket } from '../../constructs/s3';
|
|
15
|
+
import { CloudFrontWebAcl } from '../../constructs/waf';
|
|
16
|
+
import { domainAsId } from '../../lib/utils';
|
|
17
|
+
|
|
18
|
+
export interface BasicAuthCredentials {
|
|
19
|
+
user: string;
|
|
20
|
+
password: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface WebGlobalStackProps {
|
|
24
|
+
envType: 'NOT_PROD' | 'PROD';
|
|
25
|
+
description?: string;
|
|
26
|
+
bucketName?: string;
|
|
27
|
+
hostedZone: string;
|
|
28
|
+
domainName: string;
|
|
29
|
+
basicAuth?: BasicAuthCredentials;
|
|
30
|
+
redirects?: CloudFrontFunctionRedirects;
|
|
31
|
+
attachWebAcl?: boolean;
|
|
32
|
+
destroy?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class WebGlobalStack extends Stack {
|
|
36
|
+
public readonly sslCertificate: Certificate;
|
|
37
|
+
public readonly s3Bucket: Bucket;
|
|
38
|
+
public readonly cloudFrontFunction: Function;
|
|
39
|
+
public readonly cloudFrontDistribution: Distribution;
|
|
40
|
+
public readonly webAcl: CfnWebACL | undefined;
|
|
41
|
+
public readonly aRecord: ARecord;
|
|
42
|
+
public readonly aaaaRecord: AaaaRecord;
|
|
43
|
+
|
|
44
|
+
constructor(scope: Construct, id: string, props: WebGlobalStackProps & StackProps) {
|
|
45
|
+
super(scope, id, props);
|
|
46
|
+
|
|
47
|
+
const hostedZone = getHostedZone(this, 'getHostedZone', props.hostedZone);
|
|
48
|
+
|
|
49
|
+
const _sslCert = new SSLCertificate(this, 'SSLCertificate', {
|
|
50
|
+
hostedZone: hostedZone,
|
|
51
|
+
domainName: props.domainName,
|
|
52
|
+
destroy: props.destroy
|
|
53
|
+
});
|
|
54
|
+
this.sslCertificate = _sslCert.certificate;
|
|
55
|
+
|
|
56
|
+
const _s3Bucket = new S3Bucket(this, 'S3Bucket', {
|
|
57
|
+
bucketName: props.bucketName ?? PhysicalName.GENERATE_IF_NEEDED,
|
|
58
|
+
destroy: props.destroy
|
|
59
|
+
});
|
|
60
|
+
this.s3Bucket = _s3Bucket.bucket;
|
|
61
|
+
|
|
62
|
+
const defaultBehavior: BehaviorOptions = {
|
|
63
|
+
origin: S3BucketOrigin.withOriginAccessControl(this.s3Bucket),
|
|
64
|
+
functionAssociations: []
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
let cffn: CloudFrontFunction;
|
|
68
|
+
if (props.basicAuth) {
|
|
69
|
+
cffn = CloudFrontFunction.createBasicAuthDefaultDoc(this, 'createBasicAuthDefaultDoc', {
|
|
70
|
+
user: props.basicAuth.user,
|
|
71
|
+
password: props.basicAuth.password,
|
|
72
|
+
redirects: props.redirects,
|
|
73
|
+
destroy: props.destroy
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
cffn = CloudFrontFunction.createDefaultDoc(this, 'createDefaultDoc', {
|
|
77
|
+
redirects: props.redirects,
|
|
78
|
+
destroy: props.destroy
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
this.cloudFrontFunction = cffn.fn;
|
|
82
|
+
|
|
83
|
+
defaultBehavior.functionAssociations!.push({
|
|
84
|
+
eventType: FunctionEventType.VIEWER_REQUEST,
|
|
85
|
+
function: this.cloudFrontFunction
|
|
86
|
+
});
|
|
87
|
+
defaultBehavior.functionAssociations!.push({
|
|
88
|
+
eventType: FunctionEventType.VIEWER_RESPONSE,
|
|
89
|
+
function: this.cloudFrontFunction
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if (props.attachWebAcl !== false) {
|
|
94
|
+
const waf = new CloudFrontWebAcl(this, 'CloudFrontWebAcl', {
|
|
95
|
+
name: `${domainAsId(props.domainName)}`,
|
|
96
|
+
destroy: props.destroy
|
|
97
|
+
});
|
|
98
|
+
this.webAcl = waf.acl;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const cfDistribution = new CloudFrontDistribution(this, 'CloudFrontDistribution', {
|
|
102
|
+
domainName: props.domainName,
|
|
103
|
+
description: props.description ?? props.domainName,
|
|
104
|
+
priceClass: props.envType,
|
|
105
|
+
certificate: this.sslCertificate,
|
|
106
|
+
defaultBehavior,
|
|
107
|
+
webAclId: this.webAcl?.attrArn
|
|
108
|
+
});
|
|
109
|
+
this.cloudFrontDistribution = cfDistribution.distribution;
|
|
110
|
+
|
|
111
|
+
const cfAlias = new CloudFrontAlias(this, 'CloudFrontAlias', {
|
|
112
|
+
distribution: cfDistribution.distribution,
|
|
113
|
+
domainName: props.domainName,
|
|
114
|
+
hostedZone: hostedZone
|
|
115
|
+
});
|
|
116
|
+
this.aRecord = cfAlias.aRecord;
|
|
117
|
+
this.aaaaRecord = cfAlias.aaaaRecord;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Stage, StageProps } from 'aws-cdk-lib/core';
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
|
|
4
|
+
import { CodeBuildPipelineProjectProps } from '../constructs/codebuild';
|
|
5
|
+
import { WebGlobalStack, WebGlobalStackProps } from '../stacks/web-static-serverless/web-global-stack';
|
|
6
|
+
import { CodePipelineStack, CodePipelineStackProps } from '../stacks/web-static-serverless/codepipeline-stack';
|
|
7
|
+
|
|
8
|
+
export interface Str8rClmStageProps extends WebGlobalStackProps, StageProps {
|
|
9
|
+
pipeline?: Omit<CodePipelineStackProps, 'codeBuildProject'> & {
|
|
10
|
+
codeBuildProject?: Omit<CodeBuildPipelineProjectProps, 'cloudFrontDistributionArn' | 's3BucketName' | 'destroy' | 'tags'>
|
|
11
|
+
}
|
|
12
|
+
destroy?: boolean;
|
|
13
|
+
tags?: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class Str8rClmStage extends Stage {
|
|
17
|
+
public readonly webStack: WebGlobalStack;
|
|
18
|
+
public readonly codePipelineStack: CodePipelineStack;
|
|
19
|
+
|
|
20
|
+
constructor(scope: Construct, id: string, props: Str8rClmStageProps) {
|
|
21
|
+
super(scope, id);
|
|
22
|
+
|
|
23
|
+
if (props.destroy === undefined) {
|
|
24
|
+
props.destroy = true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.webStack = new WebGlobalStack(this, 'web-global', {
|
|
28
|
+
...props,
|
|
29
|
+
env: {
|
|
30
|
+
account: props.env?.account,
|
|
31
|
+
region: 'us-east-1'
|
|
32
|
+
},
|
|
33
|
+
stackName: `${props.stageName}-web-global-stack`,
|
|
34
|
+
destroy: props.destroy,
|
|
35
|
+
tags: props.tags
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (props.pipeline) {
|
|
39
|
+
props.pipeline.codeBuildProject = props.pipeline.codeBuildProject ?? {};
|
|
40
|
+
if (!props.pipeline.codeBuildProject.ecrBuildImage && !props.pipeline.codeBuildProject.buildImage) {
|
|
41
|
+
props.pipeline.codeBuildProject.ecrBuildImage = { repository: 'str8r-v3/codebuild', tag: 'veeva-clm' };
|
|
42
|
+
}
|
|
43
|
+
(props.pipeline.codeBuildProject as CodeBuildPipelineProjectProps).cloudFrontDistributionArn = this.webStack.cloudFrontDistribution.distributionArn;
|
|
44
|
+
(props.pipeline.codeBuildProject as CodeBuildPipelineProjectProps).s3BucketName = this.webStack.s3Bucket.bucketName;
|
|
45
|
+
|
|
46
|
+
this.codePipelineStack = new CodePipelineStack(this, 'codepipeline', {
|
|
47
|
+
...props.pipeline,
|
|
48
|
+
env: props.env,
|
|
49
|
+
stackName: `${props.stageName}-codepipeline-stack`,
|
|
50
|
+
destroy: props.destroy,
|
|
51
|
+
tags: props.tags
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Stage, StageProps } from 'aws-cdk-lib/core';
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
|
|
4
|
+
import { CodeBuildPipelineProjectProps } from '../constructs/codebuild';
|
|
5
|
+
import { WebGlobalStack, WebGlobalStackProps } from '../stacks/web-static-serverless/web-global-stack';
|
|
6
|
+
// import { WebRegionalStack } from '../stacks/web-static-serverless/web-regional-stack';
|
|
7
|
+
import { CodePipelineStack, CodePipelineStackProps } from '../stacks/web-static-serverless/codepipeline-stack';
|
|
8
|
+
|
|
9
|
+
export interface WebStaticServerlessStageProps extends WebGlobalStackProps, StageProps {
|
|
10
|
+
pipeline?: Omit<CodePipelineStackProps, 'codeBuildProject'> & {
|
|
11
|
+
codeBuildProject?: Omit<CodeBuildPipelineProjectProps, 'cloudFrontDistributionArn' | 's3BucketName' | 'destroy' | 'tags'>
|
|
12
|
+
}
|
|
13
|
+
destroy?: boolean;
|
|
14
|
+
tags?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class WebStaticServerlessStage extends Stage {
|
|
18
|
+
// public readonly regionalStack: WebRegionalStack;
|
|
19
|
+
public readonly webStack: WebGlobalStack;
|
|
20
|
+
public readonly codePipelineStack: CodePipelineStack;
|
|
21
|
+
|
|
22
|
+
constructor(scope: Construct, id: string, props: WebStaticServerlessStageProps) {
|
|
23
|
+
super(scope, id);
|
|
24
|
+
|
|
25
|
+
if (props.destroy === undefined) {
|
|
26
|
+
props.destroy = true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.webStack = new WebGlobalStack(this, 'web-global', {
|
|
30
|
+
...props,
|
|
31
|
+
env: {
|
|
32
|
+
account: props.env?.account,
|
|
33
|
+
region: 'us-east-1'
|
|
34
|
+
},
|
|
35
|
+
stackName: `${props.stageName}-web-global-stack`,
|
|
36
|
+
// s3BucketName: this.regionalStack.s3Bucket.bucketName
|
|
37
|
+
destroy: props.destroy,
|
|
38
|
+
tags: props.tags
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (props.pipeline) {
|
|
42
|
+
props.pipeline.codeBuildProject = props.pipeline.codeBuildProject ?? {};
|
|
43
|
+
(props.pipeline.codeBuildProject as CodeBuildPipelineProjectProps).cloudFrontDistributionArn = this.webStack.cloudFrontDistribution.distributionArn;
|
|
44
|
+
(props.pipeline.codeBuildProject as CodeBuildPipelineProjectProps).s3BucketName = this.webStack.s3Bucket.bucketName;
|
|
45
|
+
|
|
46
|
+
this.codePipelineStack = new CodePipelineStack(this, 'codepipeline', {
|
|
47
|
+
...props.pipeline,
|
|
48
|
+
env: props.env,
|
|
49
|
+
stackName: `${props.stageName}-codepipeline-stack`,
|
|
50
|
+
destroy: props.destroy,
|
|
51
|
+
tags: props.tags
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewer Request - handle basic authorization, redirects, append index.html to origin
|
|
3
|
+
* Viewer Response - handle 301 redirect with request querystring
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const DEFAULT_DOCUMENT = "__DEFAULT_DOCUMENT__";
|
|
7
|
+
const BASIC_AUTH = ["Basic __CREDENTIALS__"];
|
|
8
|
+
|
|
9
|
+
/** @type {Record<string, string | [string, 301|302]>} */
|
|
10
|
+
const redirects = __REDIRECTS__;
|
|
11
|
+
|
|
12
|
+
const useTrailingSlash = __TRAILING_SLASH__;
|
|
13
|
+
const shouldRedirDefDoc = __REDIR_DEF_DOC__;
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
function handler(event) {
|
|
17
|
+
switch (event.context.eventType) {
|
|
18
|
+
case "viewer-request":
|
|
19
|
+
return handleRequest(event);
|
|
20
|
+
case "viewer-response":
|
|
21
|
+
return handleResponse(event);
|
|
22
|
+
default:
|
|
23
|
+
return {
|
|
24
|
+
statusCode: 400,
|
|
25
|
+
statusDescription: "Bad request",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function handleRequest(event) {
|
|
31
|
+
if (authorized(event)) {
|
|
32
|
+
const request = event.request;
|
|
33
|
+
const uri = request.uri;
|
|
34
|
+
|
|
35
|
+
const redirect = redirects ? redirects[uri] : null;
|
|
36
|
+
if (redirect) {
|
|
37
|
+
const redir = Array.isArray(redirect) ? redirect : [redirect, 301];
|
|
38
|
+
return redirectResponse(event, redir[0], redir[1]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (/^(?:.+?)\.[^\.\/]+$/.test(uri)) {
|
|
42
|
+
// uri has a file extension
|
|
43
|
+
if (uri.endsWith(DEFAULT_DOCUMENT) && shouldRedirDefDoc === true) {
|
|
44
|
+
return redirectResponse(event, uri.slice(0, -1 * DEFAULT_DOCUMENT.length - (useTrailingSlash ? 0 : 1)));
|
|
45
|
+
} else {
|
|
46
|
+
return request;
|
|
47
|
+
}
|
|
48
|
+
} else if (useTrailingSlash === true && !uri.endsWith("/")) {
|
|
49
|
+
return redirectResponse(event, `${uri}/`);
|
|
50
|
+
} else if (uri !== '/' && useTrailingSlash === false && uri.endsWith("/")) {
|
|
51
|
+
return redirectResponse(event, uri.slice(0, -1));
|
|
52
|
+
} else {
|
|
53
|
+
request.uri = `${uri}${uri.endsWith("/") ? "" : "/"}${DEFAULT_DOCUMENT}`;
|
|
54
|
+
return request;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
return {
|
|
58
|
+
statusCode: 401,
|
|
59
|
+
statusDescription: "Unauthorized",
|
|
60
|
+
headers: {
|
|
61
|
+
"www-authenticate": { value: "Basic" },
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function authorized(event) {
|
|
68
|
+
let flag = false;
|
|
69
|
+
const authHeader = event.request.headers["authorization"];
|
|
70
|
+
if (authHeader && authHeader.value) {
|
|
71
|
+
BASIC_AUTH.every((val) => {
|
|
72
|
+
flag = authHeader.value === val;
|
|
73
|
+
return !flag;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return flag;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function handleResponse(event) {
|
|
80
|
+
switch (event.response.statusCode) {
|
|
81
|
+
case 301:
|
|
82
|
+
case 302:
|
|
83
|
+
return redirectResponse(event);
|
|
84
|
+
default:
|
|
85
|
+
return event.response;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function redirectResponse(event, location, statusCode) {
|
|
90
|
+
const response = event.response || {};
|
|
91
|
+
if (statusCode === 302) {
|
|
92
|
+
response.statusCode = 302;
|
|
93
|
+
response.statusDescription = "Found";
|
|
94
|
+
} else {
|
|
95
|
+
response.statusCode = 301;
|
|
96
|
+
response.statusDescription = "Moved Permanently";
|
|
97
|
+
}
|
|
98
|
+
const path = location || getLocationHeaderValue(response.headers) || '/';
|
|
99
|
+
const querystring = event.request.querystring;
|
|
100
|
+
response.headers = updateLocationHeader(response.headers, pathWithQS(path, querystring));
|
|
101
|
+
return response;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getLocationHeaderValue(headers) {
|
|
105
|
+
return headers && headers["location"] ? headers["location"].value : undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function updateLocationHeader(headers, location) {
|
|
109
|
+
headers = headers || {};
|
|
110
|
+
headers["location"] = {
|
|
111
|
+
value: location,
|
|
112
|
+
};
|
|
113
|
+
return headers;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function qsParamExists(path, key, value) {
|
|
117
|
+
return path.indexOf(`${key}=${value}`) != -1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function pathWithQS(path, querystring) {
|
|
121
|
+
let qs = [];
|
|
122
|
+
if (querystring) {
|
|
123
|
+
qs = Object.keys(querystring)
|
|
124
|
+
.map(function (key) {
|
|
125
|
+
if (querystring[key].multiValue) {
|
|
126
|
+
return querystring[key].multiValue.map((multiKey) => {
|
|
127
|
+
return !qsParamExists(path, key, multiKey.value)
|
|
128
|
+
? `${key}=${multiKey.value}`
|
|
129
|
+
: null;
|
|
130
|
+
});
|
|
131
|
+
} else {
|
|
132
|
+
return !qsParamExists(path, key, querystring[key].value)
|
|
133
|
+
? `${key}=${querystring[key].value}`
|
|
134
|
+
: null;
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
.reduce((r, item) => {
|
|
138
|
+
(item instanceof Array ? item : [item]).forEach((i) => {
|
|
139
|
+
r.push(i);
|
|
140
|
+
});
|
|
141
|
+
return r;
|
|
142
|
+
}, [])
|
|
143
|
+
.filter((item) => {
|
|
144
|
+
return item !== null;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return `${path}${qs.length > 0 ? (path.indexOf("?") == -1 ? "?" : "&") : ""}${qs.join("&")}`;
|
|
148
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewer Request - redirects, append index.html to origin
|
|
3
|
+
* Viewer Response - handle 301 redirect with request querystring
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const DEFAULT_DOCUMENT = "__DEFAULT_DOCUMENT__";
|
|
7
|
+
|
|
8
|
+
/** @type {Record<string, string | [string, 301|302]>} */
|
|
9
|
+
const redirects = __REDIRECTS__;
|
|
10
|
+
|
|
11
|
+
const useTrailingSlash = __TRAILING_SLASH__;
|
|
12
|
+
const shouldRedirDefDoc = __REDIR_DEF_DOC__;
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
function handler(event) {
|
|
16
|
+
switch (event.context.eventType) {
|
|
17
|
+
case "viewer-request":
|
|
18
|
+
return handleRequest(event);
|
|
19
|
+
case "viewer-response":
|
|
20
|
+
return handleResponse(event);
|
|
21
|
+
default:
|
|
22
|
+
return {
|
|
23
|
+
statusCode: 400,
|
|
24
|
+
statusDescription: "Bad request",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function handleRequest(event) {
|
|
30
|
+
const request = event.request;
|
|
31
|
+
const uri = request.uri;
|
|
32
|
+
|
|
33
|
+
const redirect = redirects ? redirects[uri] : null;
|
|
34
|
+
if (redirect) {
|
|
35
|
+
const redir = Array.isArray(redirect) ? redirect : [redirect, 301];
|
|
36
|
+
return redirectResponse(event, redir[0], redir[1]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (/^(?:.+?)\.[^\.\/]+$/.test(uri)) {
|
|
40
|
+
// uri has a file extension
|
|
41
|
+
if (uri.endsWith(DEFAULT_DOCUMENT) && shouldRedirDefDoc === true) {
|
|
42
|
+
return redirectResponse(event, uri.slice(0, -1 * DEFAULT_DOCUMENT.length - (useTrailingSlash ? 0 : 1)));
|
|
43
|
+
} else {
|
|
44
|
+
return request;
|
|
45
|
+
}
|
|
46
|
+
} else if (useTrailingSlash === true && !uri.endsWith("/")) {
|
|
47
|
+
return redirectResponse(event, `${uri}/`);
|
|
48
|
+
} else if (uri !== '/' && useTrailingSlash === false && uri.endsWith("/")) {
|
|
49
|
+
return redirectResponse(event, uri.slice(0, -1));
|
|
50
|
+
} else {
|
|
51
|
+
request.uri = `${uri}${uri.endsWith("/") ? "" : "/"}${DEFAULT_DOCUMENT}`;
|
|
52
|
+
return request;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function handleResponse(event) {
|
|
57
|
+
switch (event.response.statusCode) {
|
|
58
|
+
case 301:
|
|
59
|
+
case 302:
|
|
60
|
+
return redirectResponse(event);
|
|
61
|
+
default:
|
|
62
|
+
return event.response;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function redirectResponse(event, location, statusCode) {
|
|
67
|
+
const response = event.response || {};
|
|
68
|
+
if (statusCode === 302) {
|
|
69
|
+
response.statusCode = 302;
|
|
70
|
+
response.statusDescription = "Found";
|
|
71
|
+
} else {
|
|
72
|
+
response.statusCode = 301;
|
|
73
|
+
response.statusDescription = "Moved Permanently";
|
|
74
|
+
}
|
|
75
|
+
const path = location || getLocationHeaderValue(response.headers) || '/';
|
|
76
|
+
const querystring = event.request.querystring;
|
|
77
|
+
response.headers = updateLocationHeader(response.headers, pathWithQS(path, querystring));
|
|
78
|
+
return response;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getLocationHeaderValue(headers) {
|
|
82
|
+
return headers && headers["location"] ? headers["location"].value : undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function updateLocationHeader(headers, location) {
|
|
86
|
+
headers = headers || {};
|
|
87
|
+
headers["location"] = {
|
|
88
|
+
value: location,
|
|
89
|
+
};
|
|
90
|
+
return headers;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function qsParamExists(path, key, value) {
|
|
94
|
+
return path.indexOf(`${key}=${value}`) != -1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function pathWithQS(path, querystring) {
|
|
98
|
+
let qs = [];
|
|
99
|
+
if (querystring) {
|
|
100
|
+
qs = Object.keys(querystring)
|
|
101
|
+
.map(function (key) {
|
|
102
|
+
if (querystring[key].multiValue) {
|
|
103
|
+
return querystring[key].multiValue.map((multiKey) => {
|
|
104
|
+
return !qsParamExists(path, key, multiKey.value)
|
|
105
|
+
? `${key}=${multiKey.value}`
|
|
106
|
+
: null;
|
|
107
|
+
});
|
|
108
|
+
} else {
|
|
109
|
+
return !qsParamExists(path, key, querystring[key].value)
|
|
110
|
+
? `${key}=${querystring[key].value}`
|
|
111
|
+
: null;
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
.reduce((r, item) => {
|
|
115
|
+
(item instanceof Array ? item : [item]).forEach((i) => {
|
|
116
|
+
r.push(i);
|
|
117
|
+
});
|
|
118
|
+
return r;
|
|
119
|
+
}, [])
|
|
120
|
+
.filter((item) => {
|
|
121
|
+
return item !== null;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return `${path}${qs.length > 0 ? (path.indexOf("?") == -1 ? "?" : "&") : ""}${qs.join("&")}`;
|
|
125
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": [
|
|
7
|
+
"es2022"
|
|
8
|
+
],
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noImplicitAny": true,
|
|
12
|
+
"strictNullChecks": true,
|
|
13
|
+
"noImplicitThis": true,
|
|
14
|
+
"alwaysStrict": true,
|
|
15
|
+
"noUnusedLocals": false,
|
|
16
|
+
"noUnusedParameters": false,
|
|
17
|
+
"noImplicitReturns": true,
|
|
18
|
+
"noFallthroughCasesInSwitch": false,
|
|
19
|
+
"inlineSourceMap": true,
|
|
20
|
+
"inlineSources": true,
|
|
21
|
+
"experimentalDecorators": true,
|
|
22
|
+
"strictPropertyInitialization": false,
|
|
23
|
+
"skipLibCheck": true,
|
|
24
|
+
"types": ["node"]
|
|
25
|
+
},
|
|
26
|
+
"exclude": [
|
|
27
|
+
"node_modules"
|
|
28
|
+
]
|
|
29
|
+
}
|