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.
Files changed (181) hide show
  1. package/README.md +118 -0
  2. package/dist/__tests__/generator/replace-tokens.spec.d.ts +1 -0
  3. package/dist/__tests__/generator/replace-tokens.spec.js +281 -0
  4. package/dist/__tests__/generator.spec.d.ts +1 -0
  5. package/dist/__tests__/generator.spec.js +162 -0
  6. package/dist/__tests__/validation/project-name.spec.d.ts +1 -0
  7. package/dist/__tests__/validation/project-name.spec.js +57 -0
  8. package/dist/__tests__/wizard.spec.d.ts +1 -0
  9. package/dist/__tests__/wizard.spec.js +232 -0
  10. package/dist/aws/iam.d.ts +75 -0
  11. package/dist/aws/iam.js +264 -0
  12. package/dist/aws/organizations.d.ts +79 -0
  13. package/dist/aws/organizations.js +168 -0
  14. package/dist/cli.d.ts +4 -0
  15. package/dist/cli.js +206 -0
  16. package/dist/commands/setup-github.d.ts +4 -0
  17. package/dist/commands/setup-github.js +185 -0
  18. package/dist/generator/copy-file.d.ts +15 -0
  19. package/dist/generator/copy-file.js +56 -0
  20. package/dist/generator/generate-project.d.ts +14 -0
  21. package/dist/generator/generate-project.js +81 -0
  22. package/dist/generator/index.d.ts +4 -0
  23. package/dist/generator/index.js +3 -0
  24. package/dist/generator/replace-tokens.d.ts +29 -0
  25. package/dist/generator/replace-tokens.js +68 -0
  26. package/dist/github/secrets.d.ts +109 -0
  27. package/dist/github/secrets.js +275 -0
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.js +6 -0
  30. package/dist/prompts/auth.d.ts +3 -0
  31. package/dist/prompts/auth.js +23 -0
  32. package/dist/prompts/aws-config.d.ts +2 -0
  33. package/dist/prompts/aws-config.js +14 -0
  34. package/dist/prompts/features.d.ts +2 -0
  35. package/dist/prompts/features.js +10 -0
  36. package/dist/prompts/github-setup.d.ts +53 -0
  37. package/dist/prompts/github-setup.js +208 -0
  38. package/dist/prompts/org-structure.d.ts +9 -0
  39. package/dist/prompts/org-structure.js +93 -0
  40. package/dist/prompts/platforms.d.ts +2 -0
  41. package/dist/prompts/platforms.js +12 -0
  42. package/dist/prompts/project-name.d.ts +2 -0
  43. package/dist/prompts/project-name.js +8 -0
  44. package/dist/prompts/theme.d.ts +2 -0
  45. package/dist/prompts/theme.js +14 -0
  46. package/dist/templates/index.d.ts +4 -0
  47. package/dist/templates/index.js +2 -0
  48. package/dist/templates/manifest.d.ts +11 -0
  49. package/dist/templates/manifest.js +99 -0
  50. package/dist/templates/tokens.d.ts +39 -0
  51. package/dist/templates/tokens.js +37 -0
  52. package/dist/templates/types.d.ts +52 -0
  53. package/dist/templates/types.js +1 -0
  54. package/dist/types.d.ts +27 -0
  55. package/dist/types.js +1 -0
  56. package/dist/validation/project-name.d.ts +1 -0
  57. package/dist/validation/project-name.js +12 -0
  58. package/dist/wizard.d.ts +2 -0
  59. package/dist/wizard.js +81 -0
  60. package/package.json +68 -0
  61. package/templates/.github/actions/build-and-test/action.yml +24 -0
  62. package/templates/.github/actions/deploy-cdk/action.yml +46 -0
  63. package/templates/.github/actions/deploy-web/action.yml +72 -0
  64. package/templates/.github/actions/setup/action.yml +29 -0
  65. package/templates/.github/pull_request_template.md +15 -0
  66. package/templates/.github/workflows/deploy-dev.yml +80 -0
  67. package/templates/.github/workflows/deploy-prod.yml +67 -0
  68. package/templates/.github/workflows/deploy-stage.yml +77 -0
  69. package/templates/.github/workflows/pull-request.yml +72 -0
  70. package/templates/.vscode/extensions.json +7 -0
  71. package/templates/.vscode/settings.json +67 -0
  72. package/templates/apps/api/.eslintrc.json +18 -0
  73. package/templates/apps/api/cdk/app.ts +93 -0
  74. package/templates/apps/api/cdk/auth/cognito-stack.ts +164 -0
  75. package/templates/apps/api/cdk/cdk.json +73 -0
  76. package/templates/apps/api/cdk/deployment-user-stack.ts +187 -0
  77. package/templates/apps/api/cdk/org-stack.ts +67 -0
  78. package/templates/apps/api/cdk/static-stack.ts +361 -0
  79. package/templates/apps/api/cdk/tsconfig.json +39 -0
  80. package/templates/apps/api/cdk/user-stack.ts +255 -0
  81. package/templates/apps/api/jest.config.ts +38 -0
  82. package/templates/apps/api/lambdas.yml +84 -0
  83. package/templates/apps/api/project.json.template +58 -0
  84. package/templates/apps/api/src/__tests__/setup.ts +10 -0
  85. package/templates/apps/api/src/handlers/users/create-user.ts +52 -0
  86. package/templates/apps/api/src/handlers/users/delete-user.ts +45 -0
  87. package/templates/apps/api/src/handlers/users/get-me.ts +72 -0
  88. package/templates/apps/api/src/handlers/users/get-user.ts +45 -0
  89. package/templates/apps/api/src/handlers/users/get-users.ts +23 -0
  90. package/templates/apps/api/src/handlers/users/index.ts +17 -0
  91. package/templates/apps/api/src/handlers/users/update-user.ts +72 -0
  92. package/templates/apps/api/src/lib/dynamo/dynamo-model.ts +504 -0
  93. package/templates/apps/api/src/lib/dynamo/index.ts +12 -0
  94. package/templates/apps/api/src/lib/dynamo/utils.ts +39 -0
  95. package/templates/apps/api/src/middleware/auth0-auth.ts +97 -0
  96. package/templates/apps/api/src/middleware/cognito-auth.ts +90 -0
  97. package/templates/apps/api/src/models/UserModel.ts +109 -0
  98. package/templates/apps/api/src/schemas/user.schema.ts +44 -0
  99. package/templates/apps/api/src/services/user-service.ts +108 -0
  100. package/templates/apps/api/src/utils/auth-context.ts +60 -0
  101. package/templates/apps/api/src/utils/common/helpers.ts +26 -0
  102. package/templates/apps/api/src/utils/lambda-handler.ts +148 -0
  103. package/templates/apps/api/src/utils/response.ts +52 -0
  104. package/templates/apps/api/src/utils/validator.ts +75 -0
  105. package/templates/apps/api/tsconfig.app.json +15 -0
  106. package/templates/apps/api/tsconfig.json +19 -0
  107. package/templates/apps/api/tsconfig.spec.json +17 -0
  108. package/templates/apps/mobile/.env.example +5 -0
  109. package/templates/apps/mobile/.eslintrc.json +33 -0
  110. package/templates/apps/mobile/app.json +33 -0
  111. package/templates/apps/mobile/assets/.gitkeep +0 -0
  112. package/templates/apps/mobile/babel.config.js +19 -0
  113. package/templates/apps/mobile/index.js +7 -0
  114. package/templates/apps/mobile/jest.config.ts +22 -0
  115. package/templates/apps/mobile/metro.config.js +35 -0
  116. package/templates/apps/mobile/package.json +22 -0
  117. package/templates/apps/mobile/project.json.template +64 -0
  118. package/templates/apps/mobile/src/App.tsx +367 -0
  119. package/templates/apps/mobile/src/__tests__/App.spec.tsx +46 -0
  120. package/templates/apps/mobile/src/__tests__/store/user-store.spec.ts +156 -0
  121. package/templates/apps/mobile/src/config/api.ts +16 -0
  122. package/templates/apps/mobile/src/store/user-store.ts +56 -0
  123. package/templates/apps/mobile/src/test-setup.ts +10 -0
  124. package/templates/apps/mobile/tsconfig.json +22 -0
  125. package/templates/apps/web/.env.example +13 -0
  126. package/templates/apps/web/.eslintrc.json +26 -0
  127. package/templates/apps/web/index.html +13 -0
  128. package/templates/apps/web/jest.config.ts +24 -0
  129. package/templates/apps/web/package.json +15 -0
  130. package/templates/apps/web/project.json.template +66 -0
  131. package/templates/apps/web/src/App.tsx +352 -0
  132. package/templates/apps/web/src/__mocks__/config/api.ts +41 -0
  133. package/templates/apps/web/src/__tests__/App.spec.tsx +240 -0
  134. package/templates/apps/web/src/__tests__/store/user-store.spec.ts +185 -0
  135. package/templates/apps/web/src/auth/auth0-provider.tsx +103 -0
  136. package/templates/apps/web/src/auth/cognito-provider.tsx +143 -0
  137. package/templates/apps/web/src/auth/index.ts +7 -0
  138. package/templates/apps/web/src/auth/use-auth.ts +16 -0
  139. package/templates/apps/web/src/config/amplify-config.ts +31 -0
  140. package/templates/apps/web/src/config/api.ts +38 -0
  141. package/templates/apps/web/src/config/auth0-config.ts +17 -0
  142. package/templates/apps/web/src/main.tsx +41 -0
  143. package/templates/apps/web/src/store/user-store.ts +56 -0
  144. package/templates/apps/web/src/styles.css +165 -0
  145. package/templates/apps/web/src/test-setup.ts +1 -0
  146. package/templates/apps/web/src/theme/index.ts +30 -0
  147. package/templates/apps/web/src/vite-env.d.ts +19 -0
  148. package/templates/apps/web/tsconfig.app.json +24 -0
  149. package/templates/apps/web/tsconfig.json +22 -0
  150. package/templates/apps/web/tsconfig.spec.json +28 -0
  151. package/templates/apps/web/vite.config.ts +87 -0
  152. package/templates/manifest.json +28 -0
  153. package/templates/packages/api-client/.eslintrc.json +18 -0
  154. package/templates/packages/api-client/jest.config.ts +13 -0
  155. package/templates/packages/api-client/package.json +8 -0
  156. package/templates/packages/api-client/project.json.template +34 -0
  157. package/templates/packages/api-client/src/__tests__/api-client.spec.ts +408 -0
  158. package/templates/packages/api-client/src/api-client.ts +201 -0
  159. package/templates/packages/api-client/src/config.ts +193 -0
  160. package/templates/packages/api-client/src/index.ts +9 -0
  161. package/templates/packages/api-client/tsconfig.json +22 -0
  162. package/templates/packages/api-client/tsconfig.lib.json +11 -0
  163. package/templates/packages/api-client/tsconfig.spec.json +14 -0
  164. package/templates/packages/common-types/.eslintrc.json +18 -0
  165. package/templates/packages/common-types/package.json +6 -0
  166. package/templates/packages/common-types/project.json.template +26 -0
  167. package/templates/packages/common-types/src/api.types.ts +24 -0
  168. package/templates/packages/common-types/src/auth.types.ts +36 -0
  169. package/templates/packages/common-types/src/common.types.ts +46 -0
  170. package/templates/packages/common-types/src/index.ts +19 -0
  171. package/templates/packages/common-types/src/lambda.types.ts +39 -0
  172. package/templates/packages/common-types/src/user.types.ts +31 -0
  173. package/templates/packages/common-types/tsconfig.json +19 -0
  174. package/templates/packages/common-types/tsconfig.lib.json +11 -0
  175. package/templates/root/.editorconfig +23 -0
  176. package/templates/root/.nvmrc +1 -0
  177. package/templates/root/eslint.config.js +61 -0
  178. package/templates/root/jest.preset.js +16 -0
  179. package/templates/root/nx.json +29 -0
  180. package/templates/root/package.json +131 -0
  181. 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
+ };