create-aws-project 1.2.1
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/README.md +118 -0
- package/dist/__tests__/generator/replace-tokens.spec.d.ts +1 -0
- package/dist/__tests__/generator/replace-tokens.spec.js +281 -0
- package/dist/__tests__/generator.spec.d.ts +1 -0
- package/dist/__tests__/generator.spec.js +162 -0
- package/dist/__tests__/validation/project-name.spec.d.ts +1 -0
- package/dist/__tests__/validation/project-name.spec.js +57 -0
- package/dist/__tests__/wizard.spec.d.ts +1 -0
- package/dist/__tests__/wizard.spec.js +232 -0
- package/dist/aws/iam.d.ts +75 -0
- package/dist/aws/iam.js +264 -0
- package/dist/aws/organizations.d.ts +79 -0
- package/dist/aws/organizations.js +168 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +206 -0
- package/dist/commands/setup-github.d.ts +4 -0
- package/dist/commands/setup-github.js +185 -0
- package/dist/generator/copy-file.d.ts +15 -0
- package/dist/generator/copy-file.js +56 -0
- package/dist/generator/generate-project.d.ts +14 -0
- package/dist/generator/generate-project.js +81 -0
- package/dist/generator/index.d.ts +4 -0
- package/dist/generator/index.js +3 -0
- package/dist/generator/replace-tokens.d.ts +29 -0
- package/dist/generator/replace-tokens.js +68 -0
- package/dist/github/secrets.d.ts +109 -0
- package/dist/github/secrets.js +275 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/prompts/auth.d.ts +3 -0
- package/dist/prompts/auth.js +23 -0
- package/dist/prompts/aws-config.d.ts +2 -0
- package/dist/prompts/aws-config.js +14 -0
- package/dist/prompts/features.d.ts +2 -0
- package/dist/prompts/features.js +10 -0
- package/dist/prompts/github-setup.d.ts +53 -0
- package/dist/prompts/github-setup.js +208 -0
- package/dist/prompts/org-structure.d.ts +9 -0
- package/dist/prompts/org-structure.js +93 -0
- package/dist/prompts/platforms.d.ts +2 -0
- package/dist/prompts/platforms.js +12 -0
- package/dist/prompts/project-name.d.ts +2 -0
- package/dist/prompts/project-name.js +8 -0
- package/dist/prompts/theme.d.ts +2 -0
- package/dist/prompts/theme.js +14 -0
- package/dist/templates/index.d.ts +4 -0
- package/dist/templates/index.js +2 -0
- package/dist/templates/manifest.d.ts +11 -0
- package/dist/templates/manifest.js +99 -0
- package/dist/templates/tokens.d.ts +39 -0
- package/dist/templates/tokens.js +37 -0
- package/dist/templates/types.d.ts +52 -0
- package/dist/templates/types.js +1 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.js +1 -0
- package/dist/validation/project-name.d.ts +1 -0
- package/dist/validation/project-name.js +12 -0
- package/dist/wizard.d.ts +2 -0
- package/dist/wizard.js +81 -0
- package/package.json +68 -0
- package/templates/.github/actions/build-and-test/action.yml +24 -0
- package/templates/.github/actions/deploy-cdk/action.yml +46 -0
- package/templates/.github/actions/deploy-web/action.yml +72 -0
- package/templates/.github/actions/setup/action.yml +29 -0
- package/templates/.github/pull_request_template.md +15 -0
- package/templates/.github/workflows/deploy-dev.yml +80 -0
- package/templates/.github/workflows/deploy-prod.yml +67 -0
- package/templates/.github/workflows/deploy-stage.yml +77 -0
- package/templates/.github/workflows/pull-request.yml +72 -0
- package/templates/.vscode/extensions.json +7 -0
- package/templates/.vscode/settings.json +67 -0
- package/templates/apps/api/.eslintrc.json +18 -0
- package/templates/apps/api/cdk/app.ts +93 -0
- package/templates/apps/api/cdk/auth/cognito-stack.ts +164 -0
- package/templates/apps/api/cdk/cdk.json +73 -0
- package/templates/apps/api/cdk/deployment-user-stack.ts +187 -0
- package/templates/apps/api/cdk/org-stack.ts +67 -0
- package/templates/apps/api/cdk/static-stack.ts +361 -0
- package/templates/apps/api/cdk/tsconfig.json +39 -0
- package/templates/apps/api/cdk/user-stack.ts +255 -0
- package/templates/apps/api/jest.config.ts +38 -0
- package/templates/apps/api/lambdas.yml +84 -0
- package/templates/apps/api/project.json.template +58 -0
- package/templates/apps/api/src/__tests__/setup.ts +10 -0
- package/templates/apps/api/src/handlers/users/create-user.ts +52 -0
- package/templates/apps/api/src/handlers/users/delete-user.ts +45 -0
- package/templates/apps/api/src/handlers/users/get-me.ts +72 -0
- package/templates/apps/api/src/handlers/users/get-user.ts +45 -0
- package/templates/apps/api/src/handlers/users/get-users.ts +23 -0
- package/templates/apps/api/src/handlers/users/index.ts +17 -0
- package/templates/apps/api/src/handlers/users/update-user.ts +72 -0
- package/templates/apps/api/src/lib/dynamo/dynamo-model.ts +504 -0
- package/templates/apps/api/src/lib/dynamo/index.ts +12 -0
- package/templates/apps/api/src/lib/dynamo/utils.ts +39 -0
- package/templates/apps/api/src/middleware/auth0-auth.ts +97 -0
- package/templates/apps/api/src/middleware/cognito-auth.ts +90 -0
- package/templates/apps/api/src/models/UserModel.ts +109 -0
- package/templates/apps/api/src/schemas/user.schema.ts +44 -0
- package/templates/apps/api/src/services/user-service.ts +108 -0
- package/templates/apps/api/src/utils/auth-context.ts +60 -0
- package/templates/apps/api/src/utils/common/helpers.ts +26 -0
- package/templates/apps/api/src/utils/lambda-handler.ts +148 -0
- package/templates/apps/api/src/utils/response.ts +52 -0
- package/templates/apps/api/src/utils/validator.ts +75 -0
- package/templates/apps/api/tsconfig.app.json +15 -0
- package/templates/apps/api/tsconfig.json +19 -0
- package/templates/apps/api/tsconfig.spec.json +17 -0
- package/templates/apps/mobile/.env.example +5 -0
- package/templates/apps/mobile/.eslintrc.json +33 -0
- package/templates/apps/mobile/app.json +33 -0
- package/templates/apps/mobile/assets/.gitkeep +0 -0
- package/templates/apps/mobile/babel.config.js +19 -0
- package/templates/apps/mobile/index.js +7 -0
- package/templates/apps/mobile/jest.config.ts +22 -0
- package/templates/apps/mobile/metro.config.js +35 -0
- package/templates/apps/mobile/package.json +22 -0
- package/templates/apps/mobile/project.json.template +64 -0
- package/templates/apps/mobile/src/App.tsx +367 -0
- package/templates/apps/mobile/src/__tests__/App.spec.tsx +46 -0
- package/templates/apps/mobile/src/__tests__/store/user-store.spec.ts +156 -0
- package/templates/apps/mobile/src/config/api.ts +16 -0
- package/templates/apps/mobile/src/store/user-store.ts +56 -0
- package/templates/apps/mobile/src/test-setup.ts +10 -0
- package/templates/apps/mobile/tsconfig.json +22 -0
- package/templates/apps/web/.env.example +13 -0
- package/templates/apps/web/.eslintrc.json +26 -0
- package/templates/apps/web/index.html +13 -0
- package/templates/apps/web/jest.config.ts +24 -0
- package/templates/apps/web/package.json +15 -0
- package/templates/apps/web/project.json.template +66 -0
- package/templates/apps/web/src/App.tsx +352 -0
- package/templates/apps/web/src/__mocks__/config/api.ts +41 -0
- package/templates/apps/web/src/__tests__/App.spec.tsx +240 -0
- package/templates/apps/web/src/__tests__/store/user-store.spec.ts +185 -0
- package/templates/apps/web/src/auth/auth0-provider.tsx +103 -0
- package/templates/apps/web/src/auth/cognito-provider.tsx +143 -0
- package/templates/apps/web/src/auth/index.ts +7 -0
- package/templates/apps/web/src/auth/use-auth.ts +16 -0
- package/templates/apps/web/src/config/amplify-config.ts +31 -0
- package/templates/apps/web/src/config/api.ts +38 -0
- package/templates/apps/web/src/config/auth0-config.ts +17 -0
- package/templates/apps/web/src/main.tsx +41 -0
- package/templates/apps/web/src/store/user-store.ts +56 -0
- package/templates/apps/web/src/styles.css +165 -0
- package/templates/apps/web/src/test-setup.ts +1 -0
- package/templates/apps/web/src/theme/index.ts +30 -0
- package/templates/apps/web/src/vite-env.d.ts +19 -0
- package/templates/apps/web/tsconfig.app.json +24 -0
- package/templates/apps/web/tsconfig.json +22 -0
- package/templates/apps/web/tsconfig.spec.json +28 -0
- package/templates/apps/web/vite.config.ts +87 -0
- package/templates/manifest.json +28 -0
- package/templates/packages/api-client/.eslintrc.json +18 -0
- package/templates/packages/api-client/jest.config.ts +13 -0
- package/templates/packages/api-client/package.json +8 -0
- package/templates/packages/api-client/project.json.template +34 -0
- package/templates/packages/api-client/src/__tests__/api-client.spec.ts +408 -0
- package/templates/packages/api-client/src/api-client.ts +201 -0
- package/templates/packages/api-client/src/config.ts +193 -0
- package/templates/packages/api-client/src/index.ts +9 -0
- package/templates/packages/api-client/tsconfig.json +22 -0
- package/templates/packages/api-client/tsconfig.lib.json +11 -0
- package/templates/packages/api-client/tsconfig.spec.json +14 -0
- package/templates/packages/common-types/.eslintrc.json +18 -0
- package/templates/packages/common-types/package.json +6 -0
- package/templates/packages/common-types/project.json.template +26 -0
- package/templates/packages/common-types/src/api.types.ts +24 -0
- package/templates/packages/common-types/src/auth.types.ts +36 -0
- package/templates/packages/common-types/src/common.types.ts +46 -0
- package/templates/packages/common-types/src/index.ts +19 -0
- package/templates/packages/common-types/src/lambda.types.ts +39 -0
- package/templates/packages/common-types/src/user.types.ts +31 -0
- package/templates/packages/common-types/tsconfig.json +19 -0
- package/templates/packages/common-types/tsconfig.lib.json +11 -0
- package/templates/root/.editorconfig +23 -0
- package/templates/root/.nvmrc +1 -0
- package/templates/root/eslint.config.js +61 -0
- package/templates/root/jest.preset.js +16 -0
- package/templates/root/nx.json +29 -0
- package/templates/root/package.json +131 -0
- package/templates/root/tsconfig.base.json +29 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import * as cdk from 'aws-cdk-lib';
|
|
2
|
+
import * as s3 from 'aws-cdk-lib/aws-s3';
|
|
3
|
+
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
|
|
4
|
+
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
|
|
5
|
+
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
|
|
6
|
+
import * as opensearchserverless from 'aws-cdk-lib/aws-opensearchserverless';
|
|
7
|
+
import { Construct } from 'constructs';
|
|
8
|
+
|
|
9
|
+
export interface StaticStackProps extends cdk.StackProps {
|
|
10
|
+
/**
|
|
11
|
+
* Environment name (e.g., dev, staging, prod)
|
|
12
|
+
*/
|
|
13
|
+
environmentName: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Static Stack for {{PROJECT_NAME_TITLE}}
|
|
18
|
+
*
|
|
19
|
+
* Creates a CloudFront distribution with:
|
|
20
|
+
* - S3 bucket for static web content (default route)
|
|
21
|
+
* - API Gateway for Lambda API (routes starting with /api)
|
|
22
|
+
*/
|
|
23
|
+
export class StaticStack extends cdk.Stack {
|
|
24
|
+
public readonly bucket: s3.Bucket;
|
|
25
|
+
public readonly api: apigateway.RestApi;
|
|
26
|
+
public readonly distribution: cloudfront.Distribution;
|
|
27
|
+
public readonly openSearchCollection: opensearchserverless.CfnCollection;
|
|
28
|
+
|
|
29
|
+
constructor(scope: Construct, id: string, props: StaticStackProps) {
|
|
30
|
+
super(scope, id, props);
|
|
31
|
+
|
|
32
|
+
const { environmentName } = props;
|
|
33
|
+
|
|
34
|
+
// Create S3 bucket for static website content
|
|
35
|
+
this.bucket = new s3.Bucket(this, 'WebContentBucket', {
|
|
36
|
+
bucketName: `{{PROJECT_NAME}}-${environmentName}-web-${this.account}`,
|
|
37
|
+
websiteIndexDocument: 'index.html',
|
|
38
|
+
websiteErrorDocument: 'index.html', // For SPA routing
|
|
39
|
+
publicReadAccess: false, // CloudFront will access via OAI
|
|
40
|
+
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
|
41
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY, // For dev environments
|
|
42
|
+
autoDeleteObjects: true, // For dev environments
|
|
43
|
+
cors: [
|
|
44
|
+
{
|
|
45
|
+
allowedMethods: [s3.HttpMethods.GET, s3.HttpMethods.HEAD],
|
|
46
|
+
allowedOrigins: ['*'],
|
|
47
|
+
allowedHeaders: ['*'],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Create API Gateway for Lambda functions
|
|
53
|
+
this.api = new apigateway.RestApi(this, 'ApiGateway', {
|
|
54
|
+
restApiName: `{{PROJECT_NAME}}-${environmentName}-api`,
|
|
55
|
+
description: `{{PROJECT_NAME_TITLE}} API - ${environmentName}`,
|
|
56
|
+
deployOptions: {
|
|
57
|
+
stageName: environmentName,
|
|
58
|
+
throttlingRateLimit: 100,
|
|
59
|
+
throttlingBurstLimit: 200,
|
|
60
|
+
metricsEnabled: true,
|
|
61
|
+
loggingLevel: apigateway.MethodLoggingLevel.INFO,
|
|
62
|
+
dataTraceEnabled: true,
|
|
63
|
+
},
|
|
64
|
+
defaultCorsPreflightOptions: {
|
|
65
|
+
allowOrigins: apigateway.Cors.ALL_ORIGINS,
|
|
66
|
+
allowMethods: apigateway.Cors.ALL_METHODS,
|
|
67
|
+
allowHeaders: [
|
|
68
|
+
'Content-Type',
|
|
69
|
+
'X-Amz-Date',
|
|
70
|
+
'Authorization',
|
|
71
|
+
'X-Api-Key',
|
|
72
|
+
'X-Amz-Security-Token',
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
// Enable request validation
|
|
76
|
+
cloudWatchRole: true,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Create a simple health check endpoint at /api/health
|
|
80
|
+
const api = this.api.root.addResource('api');
|
|
81
|
+
const health = api.addResource('health');
|
|
82
|
+
health.addMethod('GET', new apigateway.MockIntegration({
|
|
83
|
+
integrationResponses: [
|
|
84
|
+
{
|
|
85
|
+
statusCode: '200',
|
|
86
|
+
responseTemplates: {
|
|
87
|
+
'application/json': JSON.stringify({
|
|
88
|
+
status: 'healthy',
|
|
89
|
+
timestamp: '$context.requestTime',
|
|
90
|
+
environment: environmentName,
|
|
91
|
+
}),
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
requestTemplates: {
|
|
96
|
+
'application/json': '{"statusCode": 200}',
|
|
97
|
+
},
|
|
98
|
+
}), {
|
|
99
|
+
methodResponses: [{ statusCode: '200' }],
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
// Create custom cache policy for API routes
|
|
104
|
+
const apiCachePolicy = new cloudfront.CachePolicy(this, 'ApiCachePolicy', {
|
|
105
|
+
cachePolicyName: `{{PROJECT_NAME}}-${environmentName}-api-cache`,
|
|
106
|
+
comment: 'Cache policy for API Gateway with query strings and headers',
|
|
107
|
+
defaultTtl: cdk.Duration.seconds(0), // No caching for API by default
|
|
108
|
+
minTtl: cdk.Duration.seconds(0),
|
|
109
|
+
maxTtl: cdk.Duration.seconds(1),
|
|
110
|
+
cookieBehavior: cloudfront.CacheCookieBehavior.none(),
|
|
111
|
+
headerBehavior: cloudfront.CacheHeaderBehavior.allowList(
|
|
112
|
+
'Authorization',
|
|
113
|
+
'Content-Type',
|
|
114
|
+
'Accept'
|
|
115
|
+
),
|
|
116
|
+
queryStringBehavior: cloudfront.CacheQueryStringBehavior.all(),
|
|
117
|
+
enableAcceptEncodingGzip: true,
|
|
118
|
+
enableAcceptEncodingBrotli: true,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Create custom origin request policy for API
|
|
122
|
+
const apiOriginRequestPolicy = new cloudfront.OriginRequestPolicy(this, 'ApiOriginRequestPolicy', {
|
|
123
|
+
originRequestPolicyName: `{{PROJECT_NAME}}-${environmentName}-api-origin`,
|
|
124
|
+
comment: 'Origin request policy for API Gateway',
|
|
125
|
+
cookieBehavior: cloudfront.OriginRequestCookieBehavior.none(),
|
|
126
|
+
headerBehavior: cloudfront.OriginRequestHeaderBehavior.allowList(
|
|
127
|
+
'Accept',
|
|
128
|
+
'Accept-Language',
|
|
129
|
+
'Content-Type',
|
|
130
|
+
'Referer',
|
|
131
|
+
'User-Agent'
|
|
132
|
+
),
|
|
133
|
+
queryStringBehavior: cloudfront.OriginRequestQueryStringBehavior.all(),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Create CloudFront function to rewrite /api to the environment stage
|
|
137
|
+
const apiRewriteFunction = new cloudfront.Function(this, 'ApiRewriteFunction', {
|
|
138
|
+
code: cloudfront.FunctionCode.fromInline(`
|
|
139
|
+
function handler(event) {
|
|
140
|
+
var request = event.request;
|
|
141
|
+
var uri = request.uri;
|
|
142
|
+
|
|
143
|
+
// Only rewrite if URI starts with /api
|
|
144
|
+
if (uri.indexOf('/api') === 0) {
|
|
145
|
+
request.uri = uri.replace('/api', '/${environmentName}');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return request;
|
|
149
|
+
}
|
|
150
|
+
`),
|
|
151
|
+
functionName: `${environmentName}-api-rewrite-function`,
|
|
152
|
+
comment: `Rewrites /api requests to /${environmentName} stage for API Gateway`
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Create CloudFront distribution
|
|
156
|
+
this.distribution = new cloudfront.Distribution(this, 'Distribution', {
|
|
157
|
+
comment: `{{PROJECT_NAME_TITLE}} - ${environmentName}`,
|
|
158
|
+
defaultRootObject: 'index.html',
|
|
159
|
+
priceClass: cloudfront.PriceClass.PRICE_CLASS_100, // North America and Europe
|
|
160
|
+
httpVersion: cloudfront.HttpVersion.HTTP2_AND_3,
|
|
161
|
+
enableIpv6: true,
|
|
162
|
+
|
|
163
|
+
// Default behavior: Serve static content from S3
|
|
164
|
+
defaultBehavior: {
|
|
165
|
+
origin: origins.S3BucketOrigin.withOriginAccessControl(this.bucket),
|
|
166
|
+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
167
|
+
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
|
|
168
|
+
cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS,
|
|
169
|
+
compress: true,
|
|
170
|
+
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
// Additional behavior: Route /api/* to API Gateway
|
|
174
|
+
additionalBehaviors: {
|
|
175
|
+
'/api/*': {
|
|
176
|
+
origin: new origins.RestApiOrigin(this.api),
|
|
177
|
+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.HTTPS_ONLY,
|
|
178
|
+
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
|
|
179
|
+
cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS,
|
|
180
|
+
compress: true,
|
|
181
|
+
cachePolicy: apiCachePolicy,
|
|
182
|
+
originRequestPolicy: apiOriginRequestPolicy,
|
|
183
|
+
// Add the function association for URL rewriting
|
|
184
|
+
functionAssociations: [{
|
|
185
|
+
function: apiRewriteFunction,
|
|
186
|
+
eventType: cloudfront.FunctionEventType.VIEWER_REQUEST
|
|
187
|
+
}]
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
// Error responses for SPA routing
|
|
192
|
+
errorResponses: [
|
|
193
|
+
{
|
|
194
|
+
httpStatus: 403,
|
|
195
|
+
responseHttpStatus: 200,
|
|
196
|
+
responsePagePath: '/index.html',
|
|
197
|
+
ttl: cdk.Duration.seconds(300),
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
httpStatus: 404,
|
|
201
|
+
responseHttpStatus: 200,
|
|
202
|
+
responsePagePath: '/index.html',
|
|
203
|
+
ttl: cdk.Duration.seconds(300),
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// OpenSearch Serverless Collection
|
|
209
|
+
const collectionName = `{{PROJECT_NAME}}-${environmentName}`;
|
|
210
|
+
|
|
211
|
+
// Encryption policy (required for all collections)
|
|
212
|
+
const encryptionPolicy = new opensearchserverless.CfnSecurityPolicy(this, 'OpenSearchEncryptionPolicy', {
|
|
213
|
+
name: `${collectionName}-encryption`,
|
|
214
|
+
type: 'encryption',
|
|
215
|
+
policy: JSON.stringify({
|
|
216
|
+
Rules: [
|
|
217
|
+
{
|
|
218
|
+
ResourceType: 'collection',
|
|
219
|
+
Resource: [`collection/${collectionName}`],
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
AWSOwnedKey: true,
|
|
223
|
+
}),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Network policy - allows public access (adjust for production)
|
|
227
|
+
const networkPolicy = new opensearchserverless.CfnSecurityPolicy(this, 'OpenSearchNetworkPolicy', {
|
|
228
|
+
name: `${collectionName}-network`,
|
|
229
|
+
type: 'network',
|
|
230
|
+
policy: JSON.stringify([
|
|
231
|
+
{
|
|
232
|
+
Rules: [
|
|
233
|
+
{
|
|
234
|
+
ResourceType: 'collection',
|
|
235
|
+
Resource: [`collection/${collectionName}`],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
ResourceType: 'dashboard',
|
|
239
|
+
Resource: [`collection/${collectionName}`],
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
AllowFromPublic: true,
|
|
243
|
+
},
|
|
244
|
+
]),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Data access policy - grants access to the collection
|
|
248
|
+
const dataAccessPolicy = new opensearchserverless.CfnAccessPolicy(this, 'OpenSearchDataAccessPolicy', {
|
|
249
|
+
name: `${collectionName}-access`,
|
|
250
|
+
type: 'data',
|
|
251
|
+
policy: JSON.stringify([
|
|
252
|
+
{
|
|
253
|
+
Rules: [
|
|
254
|
+
{
|
|
255
|
+
ResourceType: 'collection',
|
|
256
|
+
Resource: [`collection/${collectionName}`],
|
|
257
|
+
Permission: [
|
|
258
|
+
'aoss:CreateCollectionItems',
|
|
259
|
+
'aoss:DeleteCollectionItems',
|
|
260
|
+
'aoss:UpdateCollectionItems',
|
|
261
|
+
'aoss:DescribeCollectionItems',
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
ResourceType: 'index',
|
|
266
|
+
Resource: [`index/${collectionName}/*`],
|
|
267
|
+
Permission: [
|
|
268
|
+
'aoss:CreateIndex',
|
|
269
|
+
'aoss:DeleteIndex',
|
|
270
|
+
'aoss:UpdateIndex',
|
|
271
|
+
'aoss:DescribeIndex',
|
|
272
|
+
'aoss:ReadDocument',
|
|
273
|
+
'aoss:WriteDocument',
|
|
274
|
+
],
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
Principal: [
|
|
278
|
+
`arn:aws:iam::${this.account}:root`,
|
|
279
|
+
],
|
|
280
|
+
},
|
|
281
|
+
]),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Create the OpenSearch Serverless collection
|
|
285
|
+
this.openSearchCollection = new opensearchserverless.CfnCollection(this, 'OpenSearchCollection', {
|
|
286
|
+
name: collectionName,
|
|
287
|
+
type: 'SEARCH', // SEARCH, TIMESERIES, or VECTORSEARCH
|
|
288
|
+
description: `OpenSearch Serverless collection for {{PROJECT_NAME_TITLE}} - ${environmentName}`,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Ensure policies are created before the collection
|
|
292
|
+
this.openSearchCollection.addDependency(encryptionPolicy);
|
|
293
|
+
this.openSearchCollection.addDependency(networkPolicy);
|
|
294
|
+
this.openSearchCollection.addDependency(dataAccessPolicy);
|
|
295
|
+
|
|
296
|
+
// Output important values
|
|
297
|
+
new cdk.CfnOutput(this, 'BucketName', {
|
|
298
|
+
value: this.bucket.bucketName,
|
|
299
|
+
description: 'S3 bucket name for web content',
|
|
300
|
+
exportName: `${environmentName}-web-bucket-name`,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
new cdk.CfnOutput(this, 'BucketArn', {
|
|
304
|
+
value: this.bucket.bucketArn,
|
|
305
|
+
description: 'S3 bucket ARN',
|
|
306
|
+
exportName: `${environmentName}-web-bucket-arn`,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
new cdk.CfnOutput(this, 'ApiUrl', {
|
|
310
|
+
value: this.api.url,
|
|
311
|
+
description: 'API Gateway URL',
|
|
312
|
+
exportName: `${environmentName}-api-url`,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
new cdk.CfnOutput(this, 'ApiId', {
|
|
316
|
+
value: this.api.restApiId,
|
|
317
|
+
description: 'API Gateway ID',
|
|
318
|
+
exportName: `${environmentName}-api-id`,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
new cdk.CfnOutput(this, 'DistributionId', {
|
|
322
|
+
value: this.distribution.distributionId,
|
|
323
|
+
description: 'CloudFront distribution ID',
|
|
324
|
+
exportName: `${environmentName}-distribution-id`,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
new cdk.CfnOutput(this, 'DistributionDomainName', {
|
|
328
|
+
value: this.distribution.distributionDomainName,
|
|
329
|
+
description: 'CloudFront distribution domain name',
|
|
330
|
+
exportName: `${environmentName}-distribution-domain`,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
new cdk.CfnOutput(this, 'WebsiteUrl', {
|
|
334
|
+
value: `https://${this.distribution.distributionDomainName}`,
|
|
335
|
+
description: 'Website URL',
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
new cdk.CfnOutput(this, 'ApiUrlViaCdn', {
|
|
339
|
+
value: `https://${this.distribution.distributionDomainName}/api`,
|
|
340
|
+
description: 'API URL via CloudFront',
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
new cdk.CfnOutput(this, 'OpenSearchCollectionEndpoint', {
|
|
344
|
+
value: this.openSearchCollection.attrCollectionEndpoint,
|
|
345
|
+
description: 'OpenSearch Serverless collection endpoint',
|
|
346
|
+
exportName: `${environmentName}-opensearch-endpoint`,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
new cdk.CfnOutput(this, 'OpenSearchDashboardEndpoint', {
|
|
350
|
+
value: this.openSearchCollection.attrDashboardEndpoint,
|
|
351
|
+
description: 'OpenSearch Serverless dashboard endpoint',
|
|
352
|
+
exportName: `${environmentName}-opensearch-dashboard`,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
new cdk.CfnOutput(this, 'OpenSearchCollectionArn', {
|
|
356
|
+
value: this.openSearchCollection.attrArn,
|
|
357
|
+
description: 'OpenSearch Serverless collection ARN',
|
|
358
|
+
exportName: `${environmentName}-opensearch-arn`,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": [
|
|
6
|
+
"es2020"
|
|
7
|
+
],
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noImplicitAny": true,
|
|
11
|
+
"strictNullChecks": true,
|
|
12
|
+
"noImplicitThis": true,
|
|
13
|
+
"alwaysStrict": true,
|
|
14
|
+
"noUnusedLocals": false,
|
|
15
|
+
"noUnusedParameters": false,
|
|
16
|
+
"noImplicitReturns": true,
|
|
17
|
+
"noFallthroughCasesInSwitch": false,
|
|
18
|
+
"moduleResolution": "node",
|
|
19
|
+
"esModuleInterop": true,
|
|
20
|
+
"skipLibCheck": true,
|
|
21
|
+
"forceConsistentCasingInFileNames": true,
|
|
22
|
+
"inlineSourceMap": true,
|
|
23
|
+
"inlineSources": true,
|
|
24
|
+
"experimentalDecorators": true,
|
|
25
|
+
"strictPropertyInitialization": false,
|
|
26
|
+
"typeRoots": [
|
|
27
|
+
"../../../node_modules/@types"
|
|
28
|
+
],
|
|
29
|
+
"outDir": "../../../dist/apps/api/cdk",
|
|
30
|
+
"rootDir": "."
|
|
31
|
+
},
|
|
32
|
+
"exclude": [
|
|
33
|
+
"node_modules",
|
|
34
|
+
"cdk.out"
|
|
35
|
+
],
|
|
36
|
+
"include": [
|
|
37
|
+
"**/*.ts"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import * as cdk from 'aws-cdk-lib';
|
|
2
|
+
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
3
|
+
import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
4
|
+
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
|
|
5
|
+
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
6
|
+
import { Construct } from 'constructs';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as yaml from 'js-yaml';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Lambda configuration from YAML
|
|
13
|
+
*/
|
|
14
|
+
export interface LambdaConfig {
|
|
15
|
+
name: string;
|
|
16
|
+
source: string;
|
|
17
|
+
handler: string;
|
|
18
|
+
method: string;
|
|
19
|
+
path: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
memorySize?: number;
|
|
22
|
+
timeout?: number;
|
|
23
|
+
environment?: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* YAML configuration structure
|
|
28
|
+
*/
|
|
29
|
+
interface LambdasYaml {
|
|
30
|
+
lambdas: {
|
|
31
|
+
'user-lambdas'?: LambdaConfig[];
|
|
32
|
+
[key: string]: LambdaConfig[] | undefined;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UserStackProps extends cdk.StackProps {
|
|
37
|
+
/**
|
|
38
|
+
* Environment name (e.g., dev, staging, prod)
|
|
39
|
+
*/
|
|
40
|
+
environmentName: string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* API Gateway to add Lambda integrations to
|
|
44
|
+
*/
|
|
45
|
+
api: apigateway.RestApi;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Path to the lambdas.yml configuration file
|
|
49
|
+
* Defaults to './lambdas.yml' relative to the project root
|
|
50
|
+
*/
|
|
51
|
+
configPath?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* User Stack for {{PROJECT_NAME_TITLE}}
|
|
56
|
+
*
|
|
57
|
+
* Creates Lambda functions and API Gateway integrations based on
|
|
58
|
+
* configuration defined in lambdas.yml
|
|
59
|
+
*/
|
|
60
|
+
export class UserStack extends cdk.Stack {
|
|
61
|
+
public readonly functions: Map<string, lambda.Function>;
|
|
62
|
+
private readonly api: apigateway.RestApi;
|
|
63
|
+
private readonly environmentName: string;
|
|
64
|
+
|
|
65
|
+
constructor(scope: Construct, id: string, props: UserStackProps) {
|
|
66
|
+
super(scope, id, props);
|
|
67
|
+
|
|
68
|
+
this.api = props.api;
|
|
69
|
+
this.environmentName = props.environmentName;
|
|
70
|
+
this.functions = new Map();
|
|
71
|
+
|
|
72
|
+
// Read Lambda configurations from YAML file
|
|
73
|
+
const configPath = props.configPath || path.join(__dirname, '../lambdas.yml');
|
|
74
|
+
const lambdaConfigs = this.loadLambdaConfigs(configPath);
|
|
75
|
+
|
|
76
|
+
// Create Lambda functions and API Gateway integrations
|
|
77
|
+
lambdaConfigs.forEach(config => {
|
|
78
|
+
this.createLambdaFunction(config);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Output Lambda function ARNs
|
|
82
|
+
this.functions.forEach((func, name) => {
|
|
83
|
+
new cdk.CfnOutput(this, `${name}Arn`, {
|
|
84
|
+
value: func.functionArn,
|
|
85
|
+
description: `ARN for ${name} Lambda function`,
|
|
86
|
+
exportName: `${this.environmentName}-${name}-arn`,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Load Lambda configurations from YAML file
|
|
93
|
+
*/
|
|
94
|
+
private loadLambdaConfigs(configPath: string): LambdaConfig[] {
|
|
95
|
+
try {
|
|
96
|
+
const fileContents = fs.readFileSync(configPath, 'utf8');
|
|
97
|
+
const config = yaml.load(fileContents) as LambdasYaml;
|
|
98
|
+
|
|
99
|
+
if (!config || !config.lambdas) {
|
|
100
|
+
throw new Error('Invalid YAML structure: expected "lambdas" object');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Load user-lambdas specifically for this stack
|
|
104
|
+
const userLambdas = config.lambdas['user-lambdas'];
|
|
105
|
+
|
|
106
|
+
if (!userLambdas || !Array.isArray(userLambdas)) {
|
|
107
|
+
throw new Error('Invalid YAML structure: expected "lambdas.user-lambdas" array');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log(`Loaded ${userLambdas.length} user Lambda configurations from ${configPath}`);
|
|
111
|
+
return userLambdas;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
throw new Error(`Failed to load Lambda configurations from ${configPath}: ${error}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create a Lambda function and API Gateway integration from configuration
|
|
119
|
+
*/
|
|
120
|
+
private createLambdaFunction(config: LambdaConfig): void {
|
|
121
|
+
const { name, source, handler, method, path: apiPath, description, memorySize, timeout, environment } = config;
|
|
122
|
+
|
|
123
|
+
// Validate configuration
|
|
124
|
+
if (!name || !source || !handler || !method || !apiPath) {
|
|
125
|
+
throw new Error(`Invalid Lambda configuration: missing required fields for ${name}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Resolve the source file path relative to the API app directory
|
|
129
|
+
// __dirname is apps/api/cdk, so ../ gets us to apps/api
|
|
130
|
+
const sourcePath = path.join(__dirname, '../', source);
|
|
131
|
+
|
|
132
|
+
// Verify source file exists
|
|
133
|
+
if (!fs.existsSync(sourcePath)) {
|
|
134
|
+
throw new Error(`Source file not found: ${sourcePath} for Lambda ${name}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Create Lambda execution role with CloudWatch Logs permissions
|
|
138
|
+
const role = new iam.Role(this, `${name}Role`, {
|
|
139
|
+
roleName: `${this.environmentName}-${name}-role`,
|
|
140
|
+
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
|
|
141
|
+
managedPolicies: [
|
|
142
|
+
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Create Lambda function using NodejsFunction for automatic TypeScript bundling
|
|
147
|
+
const lambdaFunction = new lambdaNodejs.NodejsFunction(this, `${name}Function`, {
|
|
148
|
+
functionName: `${this.environmentName}-${name}`,
|
|
149
|
+
entry: sourcePath,
|
|
150
|
+
handler: handler,
|
|
151
|
+
runtime: lambda.Runtime.NODEJS_20_X,
|
|
152
|
+
role,
|
|
153
|
+
memorySize: memorySize || 256,
|
|
154
|
+
timeout: cdk.Duration.seconds(timeout || 30),
|
|
155
|
+
environment: {
|
|
156
|
+
NODE_ENV: this.environmentName,
|
|
157
|
+
...environment,
|
|
158
|
+
},
|
|
159
|
+
description: description || `${name} Lambda function`,
|
|
160
|
+
tracing: lambda.Tracing.ACTIVE, // Enable X-Ray tracing
|
|
161
|
+
bundling: {
|
|
162
|
+
minify: true,
|
|
163
|
+
sourceMap: true,
|
|
164
|
+
target: 'es2020',
|
|
165
|
+
externalModules: [],
|
|
166
|
+
format: lambdaNodejs.OutputFormat.CJS,
|
|
167
|
+
tsconfig: path.join(__dirname, '../tsconfig.app.json'),
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Store function reference
|
|
172
|
+
this.functions.set(name, lambdaFunction);
|
|
173
|
+
|
|
174
|
+
// Create API Gateway integration
|
|
175
|
+
this.createApiIntegration(lambdaFunction, method, apiPath, name);
|
|
176
|
+
|
|
177
|
+
console.log(`Created Lambda function: ${name} from ${source} (${method} ${apiPath})`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Create API Gateway integration for a Lambda function
|
|
182
|
+
*/
|
|
183
|
+
private createApiIntegration(
|
|
184
|
+
lambdaFunction: lambda.Function,
|
|
185
|
+
method: string,
|
|
186
|
+
apiPath: string,
|
|
187
|
+
name: string
|
|
188
|
+
): void {
|
|
189
|
+
// Parse the API path to create/get resources
|
|
190
|
+
const pathParts = apiPath.split('/').filter(part => part.length > 0);
|
|
191
|
+
let currentResource: apigateway.IResource = this.api.root;
|
|
192
|
+
|
|
193
|
+
// Create nested resources as needed
|
|
194
|
+
for (const part of pathParts) {
|
|
195
|
+
const existingResource = currentResource.getResource(part);
|
|
196
|
+
if (existingResource) {
|
|
197
|
+
currentResource = existingResource;
|
|
198
|
+
} else {
|
|
199
|
+
currentResource = currentResource.addResource(part);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Create Lambda integration
|
|
204
|
+
const integration = new apigateway.LambdaIntegration(lambdaFunction, {
|
|
205
|
+
proxy: true,
|
|
206
|
+
allowTestInvoke: true,
|
|
207
|
+
integrationResponses: [
|
|
208
|
+
{
|
|
209
|
+
statusCode: '200',
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Add method to resource
|
|
215
|
+
// Note: CORS OPTIONS methods are automatically added by the API Gateway's
|
|
216
|
+
// defaultCorsPreflightOptions configuration in StaticStack
|
|
217
|
+
const apiMethod = currentResource.addMethod(method.toUpperCase(), integration, {
|
|
218
|
+
methodResponses: [
|
|
219
|
+
{
|
|
220
|
+
statusCode: '200',
|
|
221
|
+
responseParameters: {
|
|
222
|
+
'method.response.header.Access-Control-Allow-Origin': true,
|
|
223
|
+
'method.response.header.Access-Control-Allow-Headers': true,
|
|
224
|
+
'method.response.header.Access-Control-Allow-Methods': true,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
statusCode: '400',
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
statusCode: '404',
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
statusCode: '500',
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
console.log(`Created API integration: ${method} ${apiPath} -> ${name}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get a Lambda function by name
|
|
244
|
+
*/
|
|
245
|
+
public getFunction(name: string): lambda.Function | undefined {
|
|
246
|
+
return this.functions.get(name);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get all Lambda functions
|
|
251
|
+
*/
|
|
252
|
+
public getAllFunctions(): lambda.Function[] {
|
|
253
|
+
return Array.from(this.functions.values());
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
displayName: 'api',
|
|
3
|
+
preset: '../../jest.preset.js',
|
|
4
|
+
testEnvironment: 'node',
|
|
5
|
+
testEnvironmentOptions: {
|
|
6
|
+
customExportConditions: ['node', 'node-addons'],
|
|
7
|
+
},
|
|
8
|
+
// Workaround for Node.js 25+ localStorage security error
|
|
9
|
+
workerIdleMemoryLimit: '512MB',
|
|
10
|
+
maxWorkers: 1,
|
|
11
|
+
transform: {
|
|
12
|
+
'^.+\\.(ts|tsx)$': [
|
|
13
|
+
'ts-jest',
|
|
14
|
+
{
|
|
15
|
+
tsconfig: '<rootDir>/tsconfig.spec.json',
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
moduleFileExtensions: ['ts', 'js'],
|
|
20
|
+
coverageDirectory: '../../coverage/apps/api',
|
|
21
|
+
// Exclude infrastructure code from coverage - tested via integration tests
|
|
22
|
+
coveragePathIgnorePatterns: [
|
|
23
|
+
'/node_modules/',
|
|
24
|
+
'/data/DynamoModel.ts',
|
|
25
|
+
'/models/UserModel.ts',
|
|
26
|
+
],
|
|
27
|
+
// Clear mocks between tests
|
|
28
|
+
clearMocks: true,
|
|
29
|
+
resetMocks: true,
|
|
30
|
+
restoreMocks: true,
|
|
31
|
+
// Setup files
|
|
32
|
+
setupFiles: ['<rootDir>/src/__tests__/setup.ts'],
|
|
33
|
+
globals: {
|
|
34
|
+
'ts-jest': {
|
|
35
|
+
isolatedModules: true,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|