@aetherionfw/infra 1.0.0 → 1.1.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.
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { Construct } from 'constructs';
|
|
2
2
|
import { TerraformStack } from 'cdktf';
|
|
3
3
|
export declare class FrameworkStack extends TerraformStack {
|
|
4
|
+
/** All created Lambda functions indexed by their function name */
|
|
5
|
+
private lambdaFunctions;
|
|
6
|
+
/** All created Cognito User Pools indexed by their props.name */
|
|
7
|
+
private cognitoPools;
|
|
4
8
|
constructor(scope: Construct, id: string);
|
|
9
|
+
private buildRestApiGateway;
|
|
10
|
+
private buildHttpApiGateway;
|
|
11
|
+
private wireRestApiRoute;
|
|
12
|
+
private wireHttpApiRoute;
|
|
13
|
+
/**
|
|
14
|
+
* Parses a path like `/users/{id}/orders` and creates intermediate
|
|
15
|
+
* API Gateway resources, reusing already-created segments.
|
|
16
|
+
*/
|
|
17
|
+
private getOrCreateRestResource;
|
|
18
|
+
private getOrCreateAuthorizer;
|
|
19
|
+
private createCorsOptionsMethod;
|
|
5
20
|
}
|
|
@@ -16,15 +16,36 @@ const iam_role_policy_1 = require("@cdktf/provider-aws/lib/iam-role-policy");
|
|
|
16
16
|
const db_instance_1 = require("@cdktf/provider-aws/lib/db-instance");
|
|
17
17
|
const lambda_function_1 = require("@cdktf/provider-aws/lib/lambda-function");
|
|
18
18
|
const lambda_event_source_mapping_1 = require("@cdktf/provider-aws/lib/lambda-event-source-mapping");
|
|
19
|
+
const lambda_permission_1 = require("@cdktf/provider-aws/lib/lambda-permission");
|
|
20
|
+
const api_gateway_rest_api_1 = require("@cdktf/provider-aws/lib/api-gateway-rest-api");
|
|
21
|
+
const api_gateway_resource_1 = require("@cdktf/provider-aws/lib/api-gateway-resource");
|
|
22
|
+
const api_gateway_method_1 = require("@cdktf/provider-aws/lib/api-gateway-method");
|
|
23
|
+
const api_gateway_integration_1 = require("@cdktf/provider-aws/lib/api-gateway-integration");
|
|
24
|
+
const api_gateway_deployment_1 = require("@cdktf/provider-aws/lib/api-gateway-deployment");
|
|
25
|
+
const api_gateway_stage_1 = require("@cdktf/provider-aws/lib/api-gateway-stage");
|
|
26
|
+
const api_gateway_authorizer_1 = require("@cdktf/provider-aws/lib/api-gateway-authorizer");
|
|
27
|
+
const api_gateway_method_response_1 = require("@cdktf/provider-aws/lib/api-gateway-method-response");
|
|
28
|
+
const api_gateway_integration_response_1 = require("@cdktf/provider-aws/lib/api-gateway-integration-response");
|
|
29
|
+
const apigatewayv2_api_1 = require("@cdktf/provider-aws/lib/apigatewayv2-api");
|
|
30
|
+
const apigatewayv2_integration_1 = require("@cdktf/provider-aws/lib/apigatewayv2-integration");
|
|
31
|
+
const apigatewayv2_route_1 = require("@cdktf/provider-aws/lib/apigatewayv2-route");
|
|
32
|
+
const apigatewayv2_stage_1 = require("@cdktf/provider-aws/lib/apigatewayv2-stage");
|
|
19
33
|
const core_1 = require("@aetherionfw/core");
|
|
20
34
|
class FrameworkStack extends cdktf_1.TerraformStack {
|
|
35
|
+
/** All created Lambda functions indexed by their function name */
|
|
36
|
+
lambdaFunctions = new Map();
|
|
37
|
+
/** All created Cognito User Pools indexed by their props.name */
|
|
38
|
+
cognitoPools = new Map();
|
|
21
39
|
constructor(scope, id) {
|
|
22
40
|
super(scope, id);
|
|
23
41
|
new provider_1.AwsProvider(this, 'AWS', {
|
|
24
42
|
region: 'us-east-1',
|
|
25
43
|
});
|
|
26
44
|
const registry = core_1.MetadataRegistry.getInstance();
|
|
45
|
+
// ────────────────────────────────────────────
|
|
27
46
|
// 1. Build Infra Resources
|
|
47
|
+
// ────────────────────────────────────────────
|
|
48
|
+
const apiGatewayConfigs = new Map();
|
|
28
49
|
const infraClasses = registry.getInfraClasses();
|
|
29
50
|
for (const target of infraClasses) {
|
|
30
51
|
const resources = registry.getInfraResources(target);
|
|
@@ -34,7 +55,6 @@ class FrameworkStack extends cdktf_1.TerraformStack {
|
|
|
34
55
|
case 'S3Bucket':
|
|
35
56
|
new s3_bucket_1.S3Bucket(this, res.name, {
|
|
36
57
|
bucket: res.props.name,
|
|
37
|
-
// Minimal abstracted configuration mapped to raw CDKTF
|
|
38
58
|
});
|
|
39
59
|
break;
|
|
40
60
|
case 'DynamoTable':
|
|
@@ -64,7 +84,6 @@ class FrameworkStack extends cdktf_1.TerraformStack {
|
|
|
64
84
|
});
|
|
65
85
|
break;
|
|
66
86
|
case 'CloudFrontDistribution':
|
|
67
|
-
// Simplified mapping for a highly complex resource
|
|
68
87
|
new cloudfront_distribution_1.CloudfrontDistribution(this, res.name, {
|
|
69
88
|
enabled: true,
|
|
70
89
|
origin: [{
|
|
@@ -83,11 +102,13 @@ class FrameworkStack extends cdktf_1.TerraformStack {
|
|
|
83
102
|
restrictions: { geoRestriction: { restrictionType: 'none' } },
|
|
84
103
|
});
|
|
85
104
|
break;
|
|
86
|
-
case 'CognitoUserPool':
|
|
87
|
-
new cognito_user_pool_1.CognitoUserPool(this, res.name, {
|
|
105
|
+
case 'CognitoUserPool': {
|
|
106
|
+
const pool = new cognito_user_pool_1.CognitoUserPool(this, res.name, {
|
|
88
107
|
name: res.props.name,
|
|
89
108
|
});
|
|
109
|
+
this.cognitoPools.set(res.props.name, pool);
|
|
90
110
|
break;
|
|
111
|
+
}
|
|
91
112
|
case 'Vpc':
|
|
92
113
|
new vpc_1.Vpc(this, res.name, {
|
|
93
114
|
cidrBlock: res.props.cidr,
|
|
@@ -99,7 +120,7 @@ class FrameworkStack extends cdktf_1.TerraformStack {
|
|
|
99
120
|
case 'IamRole':
|
|
100
121
|
new iam_role_1.IamRole(this, res.name, {
|
|
101
122
|
name: res.props.name,
|
|
102
|
-
assumeRolePolicy: res.props.assumedBy,
|
|
123
|
+
assumeRolePolicy: res.props.assumedBy,
|
|
103
124
|
});
|
|
104
125
|
break;
|
|
105
126
|
case 'RdsInstance':
|
|
@@ -107,23 +128,27 @@ class FrameworkStack extends cdktf_1.TerraformStack {
|
|
|
107
128
|
engine: res.props.engine,
|
|
108
129
|
instanceClass: res.props.size,
|
|
109
130
|
dbName: res.props.dbName,
|
|
110
|
-
skipFinalSnapshot: true,
|
|
131
|
+
skipFinalSnapshot: true,
|
|
111
132
|
});
|
|
112
133
|
break;
|
|
134
|
+
case 'ApiGateway':
|
|
135
|
+
// Collect for later processing (after Lambdas are created)
|
|
136
|
+
apiGatewayConfigs.set(res.props.name, { infraResource: res, props: res.props });
|
|
137
|
+
break;
|
|
113
138
|
default:
|
|
114
139
|
console.warn(`Unknown infra resource type: ${res.type}`);
|
|
115
140
|
}
|
|
116
141
|
}
|
|
117
142
|
}
|
|
143
|
+
// ────────────────────────────────────────────
|
|
118
144
|
// 2. Build Lambdas and IAM Policies (1:1 Lambda per Handle Architecture)
|
|
145
|
+
// ────────────────────────────────────────────
|
|
119
146
|
const controllers = registry.getControllers();
|
|
120
147
|
for (const [target, controllerMeta] of controllers) {
|
|
121
148
|
const handles = registry.getHandles(target);
|
|
122
149
|
const iamPermissions = registry.getIamPermissions(target);
|
|
123
|
-
const routes = registry.getRoutes(target);
|
|
124
150
|
const sqsTriggers = registry.getSqsTriggers(target);
|
|
125
151
|
if (handles.length === 0) {
|
|
126
|
-
// Fallback or skip if no methods are decorated with @Handle
|
|
127
152
|
continue;
|
|
128
153
|
}
|
|
129
154
|
for (const handle of handles) {
|
|
@@ -145,7 +170,6 @@ class FrameworkStack extends cdktf_1.TerraformStack {
|
|
|
145
170
|
// 2b. Attach IAM Policies (Method overrides Class)
|
|
146
171
|
let handlePerms = iamPermissions.find(p => p.methodName === methodName);
|
|
147
172
|
if (!handlePerms) {
|
|
148
|
-
// Fallback to class-level permissions
|
|
149
173
|
handlePerms = iamPermissions.find(p => !p.methodName);
|
|
150
174
|
}
|
|
151
175
|
if (handlePerms) {
|
|
@@ -178,26 +202,344 @@ class FrameworkStack extends cdktf_1.TerraformStack {
|
|
|
178
202
|
memorySize: handle.memorySize || controllerMeta.memorySize || 128,
|
|
179
203
|
timeout: handle.timeout || controllerMeta.timeout || 3,
|
|
180
204
|
role: role.arn,
|
|
181
|
-
filename: 'dummy.zip',
|
|
205
|
+
filename: 'dummy.zip',
|
|
182
206
|
handler: 'index.handler',
|
|
183
207
|
environment: {
|
|
184
208
|
variables: {
|
|
185
|
-
AETHERION_TARGET_CLASS: controllerMeta.lambdaName,
|
|
209
|
+
AETHERION_TARGET_CLASS: controllerMeta.lambdaName,
|
|
186
210
|
AETHERION_TARGET_METHOD: methodName,
|
|
187
211
|
}
|
|
188
212
|
}
|
|
189
213
|
});
|
|
214
|
+
this.lambdaFunctions.set(lambdaName, lambdaFunction);
|
|
190
215
|
// 2d. Check for SQS Triggers targeting this handle
|
|
191
216
|
const handleTriggers = sqsTriggers.filter(t => t.methodName === methodName);
|
|
192
217
|
for (const trigger of handleTriggers) {
|
|
193
218
|
console.log(`Linking SQS Trigger ${trigger.queueName} to ${lambdaName}`);
|
|
194
219
|
new lambda_event_source_mapping_1.LambdaEventSourceMapping(this, `${lambdaName}-${trigger.queueName}-mapping`, {
|
|
195
220
|
functionName: lambdaFunction.arn,
|
|
196
|
-
eventSourceArn: `arn:aws:sqs:us-east-1:123456789012:${trigger.queueName}`,
|
|
221
|
+
eventSourceArn: `arn:aws:sqs:us-east-1:123456789012:${trigger.queueName}`,
|
|
197
222
|
});
|
|
198
223
|
}
|
|
199
224
|
}
|
|
200
225
|
}
|
|
226
|
+
// ────────────────────────────────────────────
|
|
227
|
+
// 3. Build API Gateways and wire Routes
|
|
228
|
+
// ────────────────────────────────────────────
|
|
229
|
+
const apiRefs = new Map();
|
|
230
|
+
// 3a. Create or import each API Gateway
|
|
231
|
+
for (const [apiName, config] of apiGatewayConfigs) {
|
|
232
|
+
const props = config.props;
|
|
233
|
+
console.log(`Building API Gateway: ${apiName} (${props.type})`);
|
|
234
|
+
if (props.type === 'REST') {
|
|
235
|
+
this.buildRestApiGateway(apiName, props, apiRefs);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
this.buildHttpApiGateway(apiName, props, apiRefs);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// 3b. Wire controllers to their API Gateways
|
|
242
|
+
for (const [target, controllerMeta] of controllers) {
|
|
243
|
+
if (!controllerMeta.apiGateway)
|
|
244
|
+
continue;
|
|
245
|
+
const apiRef = apiRefs.get(controllerMeta.apiGateway);
|
|
246
|
+
if (!apiRef) {
|
|
247
|
+
console.warn(`API Gateway "${controllerMeta.apiGateway}" not found for controller "${controllerMeta.lambdaName}"`);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const routes = registry.getRoutes(target);
|
|
251
|
+
const handles = registry.getHandles(target);
|
|
252
|
+
for (const route of routes) {
|
|
253
|
+
// Find the matching handle for this route to get the lambda name
|
|
254
|
+
const handle = handles.find(h => h.methodName === route.methodName);
|
|
255
|
+
if (!handle)
|
|
256
|
+
continue;
|
|
257
|
+
const lambdaName = `${controllerMeta.lambdaName}-${route.methodName}`;
|
|
258
|
+
const lambdaFn = this.lambdaFunctions.get(lambdaName);
|
|
259
|
+
if (!lambdaFn)
|
|
260
|
+
continue;
|
|
261
|
+
console.log(`Wiring ${route.method} ${route.path} → ${lambdaName}`);
|
|
262
|
+
if (apiRef.metadata.type === 'REST') {
|
|
263
|
+
this.wireRestApiRoute(apiRef, route, lambdaFn, lambdaName);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
this.wireHttpApiRoute(apiRef, route, lambdaFn, lambdaName);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// 3c. Create Deployments and Stages
|
|
271
|
+
for (const [apiName, apiRef] of apiRefs) {
|
|
272
|
+
const stageName = apiRef.metadata.stageName || 'dev';
|
|
273
|
+
if (apiRef.metadata.type === 'REST' && apiRef.restApi && !apiRef.metadata.existingApiId) {
|
|
274
|
+
const deployment = new api_gateway_deployment_1.ApiGatewayDeployment(this, `${apiName}-deployment`, {
|
|
275
|
+
restApiId: apiRef.restApi.id,
|
|
276
|
+
lifecycle: {
|
|
277
|
+
createBeforeDestroy: true,
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
new api_gateway_stage_1.ApiGatewayStage(this, `${apiName}-stage`, {
|
|
281
|
+
restApiId: apiRef.restApi.id,
|
|
282
|
+
deploymentId: deployment.id,
|
|
283
|
+
stageName,
|
|
284
|
+
});
|
|
285
|
+
console.log(`Created REST API deployment: ${apiName} → stage "${stageName}"`);
|
|
286
|
+
}
|
|
287
|
+
else if (apiRef.metadata.type === 'HTTP' && apiRef.httpApi) {
|
|
288
|
+
new apigatewayv2_stage_1.Apigatewayv2Stage(this, `${apiName}-stage`, {
|
|
289
|
+
apiId: apiRef.httpApi.id,
|
|
290
|
+
name: stageName,
|
|
291
|
+
autoDeploy: true,
|
|
292
|
+
});
|
|
293
|
+
console.log(`Created HTTP API stage: ${apiName} → "${stageName}"`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// ────────────────────────────────────────────
|
|
298
|
+
// REST API Gateway Builder
|
|
299
|
+
// ────────────────────────────────────────────
|
|
300
|
+
buildRestApiGateway(apiName, props, apiRefs) {
|
|
301
|
+
if (props.existingApiId) {
|
|
302
|
+
// Import existing REST API
|
|
303
|
+
console.log(`Importing existing REST API: ${props.existingApiId}`);
|
|
304
|
+
apiRefs.set(apiName, {
|
|
305
|
+
metadata: props,
|
|
306
|
+
rootResourceId: props.existingRootResourceId || '',
|
|
307
|
+
resourceMap: new Map(),
|
|
308
|
+
authorizerMap: new Map(),
|
|
309
|
+
methodIds: [],
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
// Create new REST API
|
|
314
|
+
const restApi = new api_gateway_rest_api_1.ApiGatewayRestApi(this, apiName, {
|
|
315
|
+
name: props.name,
|
|
316
|
+
description: props.description || `API Gateway for ${props.name}`,
|
|
317
|
+
});
|
|
318
|
+
apiRefs.set(apiName, {
|
|
319
|
+
metadata: props,
|
|
320
|
+
restApi,
|
|
321
|
+
rootResourceId: restApi.rootResourceId,
|
|
322
|
+
resourceMap: new Map(),
|
|
323
|
+
authorizerMap: new Map(),
|
|
324
|
+
methodIds: [],
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// ────────────────────────────────────────────
|
|
329
|
+
// HTTP API Gateway Builder
|
|
330
|
+
// ────────────────────────────────────────────
|
|
331
|
+
buildHttpApiGateway(apiName, props, apiRefs) {
|
|
332
|
+
if (props.existingApiId) {
|
|
333
|
+
console.log(`Importing existing HTTP API: ${props.existingApiId}`);
|
|
334
|
+
apiRefs.set(apiName, {
|
|
335
|
+
metadata: props,
|
|
336
|
+
rootResourceId: '',
|
|
337
|
+
resourceMap: new Map(),
|
|
338
|
+
authorizerMap: new Map(),
|
|
339
|
+
methodIds: [],
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
const corsConfig = props.corsEnabled !== false ? {
|
|
344
|
+
allowOrigins: props.corsOrigins || ['*'],
|
|
345
|
+
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
346
|
+
allowHeaders: ['Content-Type', 'Authorization', 'X-Amz-Date', 'X-Api-Key'],
|
|
347
|
+
} : undefined;
|
|
348
|
+
const httpApi = new apigatewayv2_api_1.Apigatewayv2Api(this, apiName, {
|
|
349
|
+
name: props.name,
|
|
350
|
+
protocolType: 'HTTP',
|
|
351
|
+
description: props.description || `HTTP API for ${props.name}`,
|
|
352
|
+
corsConfiguration: corsConfig,
|
|
353
|
+
});
|
|
354
|
+
apiRefs.set(apiName, {
|
|
355
|
+
metadata: props,
|
|
356
|
+
httpApi,
|
|
357
|
+
rootResourceId: '',
|
|
358
|
+
resourceMap: new Map(),
|
|
359
|
+
authorizerMap: new Map(),
|
|
360
|
+
methodIds: [],
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// ────────────────────────────────────────────
|
|
365
|
+
// REST API Route Wiring
|
|
366
|
+
// ────────────────────────────────────────────
|
|
367
|
+
wireRestApiRoute(apiRef, route, lambdaFn, lambdaName) {
|
|
368
|
+
const restApi = apiRef.restApi;
|
|
369
|
+
const restApiId = restApi ? restApi.id : apiRef.metadata.existingApiId;
|
|
370
|
+
// Build hierarchical resources for the path
|
|
371
|
+
const resourceId = this.getOrCreateRestResource(apiRef, route.path, restApiId);
|
|
372
|
+
// Determine authorization type
|
|
373
|
+
let authorizationType = 'NONE';
|
|
374
|
+
let authorizerId;
|
|
375
|
+
if (route.authorizer) {
|
|
376
|
+
const authorizer = this.getOrCreateAuthorizer(apiRef, route.authorizer, restApiId);
|
|
377
|
+
if (authorizer) {
|
|
378
|
+
authorizationType = 'COGNITO_USER_POOLS';
|
|
379
|
+
authorizerId = authorizer.id;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const methodId = `${lambdaName}-${route.method}`;
|
|
383
|
+
// Create Method
|
|
384
|
+
const method = new api_gateway_method_1.ApiGatewayMethod(this, methodId, {
|
|
385
|
+
restApiId,
|
|
386
|
+
resourceId,
|
|
387
|
+
httpMethod: route.method.toUpperCase(),
|
|
388
|
+
authorization: authorizationType,
|
|
389
|
+
authorizerId,
|
|
390
|
+
});
|
|
391
|
+
apiRef.methodIds.push(methodId);
|
|
392
|
+
// Create Integration (AWS_PROXY)
|
|
393
|
+
new api_gateway_integration_1.ApiGatewayIntegration(this, `${methodId}-integration`, {
|
|
394
|
+
restApiId,
|
|
395
|
+
resourceId,
|
|
396
|
+
httpMethod: method.httpMethod,
|
|
397
|
+
type: 'AWS_PROXY',
|
|
398
|
+
integrationHttpMethod: 'POST',
|
|
399
|
+
uri: lambdaFn.invokeArn,
|
|
400
|
+
});
|
|
401
|
+
// Grant API Gateway permission to invoke Lambda
|
|
402
|
+
new lambda_permission_1.LambdaPermission(this, `${methodId}-permission`, {
|
|
403
|
+
statementId: `AllowAPIGateway-${methodId}`,
|
|
404
|
+
action: 'lambda:InvokeFunction',
|
|
405
|
+
functionName: lambdaFn.functionName,
|
|
406
|
+
principal: 'apigateway.amazonaws.com',
|
|
407
|
+
});
|
|
408
|
+
// CORS: Create OPTIONS method if CORS is enabled
|
|
409
|
+
if (apiRef.metadata.corsEnabled !== false) {
|
|
410
|
+
this.createCorsOptionsMethod(apiRef, route.path, restApiId, resourceId, lambdaName);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// ────────────────────────────────────────────
|
|
414
|
+
// HTTP API Route Wiring
|
|
415
|
+
// ────────────────────────────────────────────
|
|
416
|
+
wireHttpApiRoute(apiRef, route, lambdaFn, lambdaName) {
|
|
417
|
+
const httpApi = apiRef.httpApi;
|
|
418
|
+
const apiId = httpApi ? httpApi.id : apiRef.metadata.existingApiId;
|
|
419
|
+
const integrationId = `${lambdaName}-${route.method}-int`;
|
|
420
|
+
// Create Integration
|
|
421
|
+
const integration = new apigatewayv2_integration_1.Apigatewayv2Integration(this, integrationId, {
|
|
422
|
+
apiId,
|
|
423
|
+
integrationType: 'AWS_PROXY',
|
|
424
|
+
integrationUri: lambdaFn.invokeArn,
|
|
425
|
+
payloadFormatVersion: '2.0',
|
|
426
|
+
});
|
|
427
|
+
// Create Route
|
|
428
|
+
const routeKey = `${route.method.toUpperCase()} ${route.path}`;
|
|
429
|
+
new apigatewayv2_route_1.Apigatewayv2Route(this, `${lambdaName}-${route.method}-route`, {
|
|
430
|
+
apiId,
|
|
431
|
+
routeKey,
|
|
432
|
+
target: `integrations/${integration.id}`,
|
|
433
|
+
});
|
|
434
|
+
// Grant API Gateway permission to invoke Lambda
|
|
435
|
+
new lambda_permission_1.LambdaPermission(this, `${lambdaName}-${route.method}-permission`, {
|
|
436
|
+
statementId: `AllowHTTPAPI-${lambdaName}-${route.method}`,
|
|
437
|
+
action: 'lambda:InvokeFunction',
|
|
438
|
+
functionName: lambdaFn.functionName,
|
|
439
|
+
principal: 'apigateway.amazonaws.com',
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
// ────────────────────────────────────────────
|
|
443
|
+
// Hierarchical Resource Builder (REST API)
|
|
444
|
+
// ────────────────────────────────────────────
|
|
445
|
+
/**
|
|
446
|
+
* Parses a path like `/users/{id}/orders` and creates intermediate
|
|
447
|
+
* API Gateway resources, reusing already-created segments.
|
|
448
|
+
*/
|
|
449
|
+
getOrCreateRestResource(apiRef, path, restApiId) {
|
|
450
|
+
if (path === '/')
|
|
451
|
+
return apiRef.rootResourceId;
|
|
452
|
+
const segments = path.split('/').filter(Boolean);
|
|
453
|
+
let currentParentId = apiRef.rootResourceId;
|
|
454
|
+
let currentPath = '';
|
|
455
|
+
for (const segment of segments) {
|
|
456
|
+
currentPath += `/${segment}`;
|
|
457
|
+
if (apiRef.resourceMap.has(currentPath)) {
|
|
458
|
+
currentParentId = apiRef.resourceMap.get(currentPath).id;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
const resource = new api_gateway_resource_1.ApiGatewayResource(this, `resource-${currentPath.replace(/[/{}]/g, '-')}`, {
|
|
462
|
+
restApiId,
|
|
463
|
+
parentId: currentParentId,
|
|
464
|
+
pathPart: segment,
|
|
465
|
+
});
|
|
466
|
+
apiRef.resourceMap.set(currentPath, resource);
|
|
467
|
+
currentParentId = resource.id;
|
|
468
|
+
}
|
|
469
|
+
return currentParentId;
|
|
470
|
+
}
|
|
471
|
+
// ────────────────────────────────────────────
|
|
472
|
+
// Cognito Authorizer Builder (REST API)
|
|
473
|
+
// ────────────────────────────────────────────
|
|
474
|
+
getOrCreateAuthorizer(apiRef, authorizerName, restApiId) {
|
|
475
|
+
// Reuse existing authorizer if already created for this API
|
|
476
|
+
if (apiRef.authorizerMap.has(authorizerName)) {
|
|
477
|
+
return apiRef.authorizerMap.get(authorizerName);
|
|
478
|
+
}
|
|
479
|
+
// Look up the Cognito User Pool by name
|
|
480
|
+
const pool = this.cognitoPools.get(authorizerName);
|
|
481
|
+
if (!pool) {
|
|
482
|
+
console.warn(`Authorizer "${authorizerName}" references a Cognito User Pool that was not found in @Infra resources.`);
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
const authorizer = new api_gateway_authorizer_1.ApiGatewayAuthorizer(this, `${authorizerName}-authorizer`, {
|
|
486
|
+
name: `${authorizerName}-cognito-authorizer`,
|
|
487
|
+
restApiId,
|
|
488
|
+
type: 'COGNITO_USER_POOLS',
|
|
489
|
+
providerArns: [pool.arn],
|
|
490
|
+
});
|
|
491
|
+
apiRef.authorizerMap.set(authorizerName, authorizer);
|
|
492
|
+
return authorizer;
|
|
493
|
+
}
|
|
494
|
+
// ────────────────────────────────────────────
|
|
495
|
+
// CORS OPTIONS Method (REST API)
|
|
496
|
+
// ────────────────────────────────────────────
|
|
497
|
+
createCorsOptionsMethod(apiRef, path, restApiId, resourceId, lambdaName) {
|
|
498
|
+
const corsId = `${lambdaName}-OPTIONS-${path.replace(/[/{}]/g, '-')}`;
|
|
499
|
+
// Avoid duplicate OPTIONS methods on the same resource
|
|
500
|
+
if (apiRef.methodIds.includes(corsId))
|
|
501
|
+
return;
|
|
502
|
+
apiRef.methodIds.push(corsId);
|
|
503
|
+
const origins = apiRef.metadata.corsOrigins?.join(',') || '*';
|
|
504
|
+
const headers = 'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token';
|
|
505
|
+
const methods = 'GET,POST,PUT,PATCH,DELETE,OPTIONS';
|
|
506
|
+
const optionsMethod = new api_gateway_method_1.ApiGatewayMethod(this, corsId, {
|
|
507
|
+
restApiId,
|
|
508
|
+
resourceId,
|
|
509
|
+
httpMethod: 'OPTIONS',
|
|
510
|
+
authorization: 'NONE',
|
|
511
|
+
});
|
|
512
|
+
new api_gateway_integration_1.ApiGatewayIntegration(this, `${corsId}-integration`, {
|
|
513
|
+
restApiId,
|
|
514
|
+
resourceId,
|
|
515
|
+
httpMethod: optionsMethod.httpMethod,
|
|
516
|
+
type: 'MOCK',
|
|
517
|
+
requestTemplates: {
|
|
518
|
+
'application/json': '{"statusCode": 200}',
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
new api_gateway_method_response_1.ApiGatewayMethodResponse(this, `${corsId}-response`, {
|
|
522
|
+
restApiId,
|
|
523
|
+
resourceId,
|
|
524
|
+
httpMethod: optionsMethod.httpMethod,
|
|
525
|
+
statusCode: '200',
|
|
526
|
+
responseParameters: {
|
|
527
|
+
'method.response.header.Access-Control-Allow-Headers': true,
|
|
528
|
+
'method.response.header.Access-Control-Allow-Methods': true,
|
|
529
|
+
'method.response.header.Access-Control-Allow-Origin': true,
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
new api_gateway_integration_response_1.ApiGatewayIntegrationResponse(this, `${corsId}-int-response`, {
|
|
533
|
+
restApiId,
|
|
534
|
+
resourceId,
|
|
535
|
+
httpMethod: optionsMethod.httpMethod,
|
|
536
|
+
statusCode: '200',
|
|
537
|
+
responseParameters: {
|
|
538
|
+
'method.response.header.Access-Control-Allow-Headers': `'${headers}'`,
|
|
539
|
+
'method.response.header.Access-Control-Allow-Methods': `'${methods}'`,
|
|
540
|
+
'method.response.header.Access-Control-Allow-Origin': `'${origins}'`,
|
|
541
|
+
},
|
|
542
|
+
});
|
|
201
543
|
}
|
|
202
544
|
}
|
|
203
545
|
exports.FrameworkStack = FrameworkStack;
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aetherionfw/infra",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "CDKTF Builder for Aetherion Framework",
|
|
5
5
|
"main": "dist/main.js",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"cdktf": "^0.20.0",
|
|
8
8
|
"constructs": "^10.3.0",
|
|
9
9
|
"@cdktf/provider-aws": "^19.0.0",
|
|
10
|
-
"@aetherionfw/core": "1.
|
|
10
|
+
"@aetherionfw/core": "1.1.0"
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|
|
13
13
|
"typescript": "^5.5.4"
|