@aetherionfw/infra 1.0.0 → 1.1.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 +45 -0
- package/dist/builder/FrameworkStack.d.ts +20 -0
- package/dist/builder/FrameworkStack.js +431 -13
- package/package.json +8 -2
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @aetherionfw/infra
|
|
2
|
+
|
|
3
|
+
> CDKTF (Cloud Development Kit for Terraform) builder for the Aetherion Serverless Framework.
|
|
4
|
+
|
|
5
|
+
Aetherion is a modern, TypeScript-first serverless framework designed to bring the developer experience of frameworks like NestJS to AWS serverless architectures.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
This package is responsible for reading the metadata generated by your `@aetherionfw/core` decorators and automatically synthesizing the corresponding AWS infrastructure using Terraform (via CDKTF).
|
|
10
|
+
|
|
11
|
+
Features:
|
|
12
|
+
- Automatic **Lambda Function** provisioning (1:1 Architecture)
|
|
13
|
+
- Automatic **API Gateway** (HTTP/REST) wiring with resources, methods, and integrations
|
|
14
|
+
- **DynamoDB**, **RDS**, **S3**, **SQS**, **EventBridge**, and **KMS** integrations
|
|
15
|
+
- Automatic **IAM Permissions** resolution
|
|
16
|
+
- **VPC** configuration support
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @aetherionfw/infra cdktf constructs @cdktf/provider-aws
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
You rarely need to interact with this package manually. The Aetherion CLI handles the synthesis process for you. However, the entry point looks like this:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { App } from 'cdktf';
|
|
30
|
+
import { FrameworkStack } from '@aetherionfw/infra';
|
|
31
|
+
import '../src/app.module'; // Import your app module to register metadata
|
|
32
|
+
|
|
33
|
+
const app = new App();
|
|
34
|
+
// This automatically synthesizes all Lambdas, API Gateways, etc.
|
|
35
|
+
new FrameworkStack(app, 'aetherion-dev');
|
|
36
|
+
app.synth();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Documentation
|
|
40
|
+
|
|
41
|
+
For full documentation, visit [https://github.com/CrisD3v/Aetherion-framework](https://github.com/CrisD3v/Aetherion-framework).
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import { Construct } from 'constructs';
|
|
2
2
|
import { TerraformStack } from 'cdktf';
|
|
3
3
|
export declare class FrameworkStack extends TerraformStack {
|
|
4
|
+
/**
|
|
5
|
+
* Resolves and loads `aetherion.config.ts` from the current working directory.
|
|
6
|
+
* Falls back to safe defaults if no config file is found.
|
|
7
|
+
*/
|
|
8
|
+
private static loadConfig;
|
|
9
|
+
/** All created Lambda functions indexed by their function name */
|
|
10
|
+
private lambdaFunctions;
|
|
11
|
+
/** All created Cognito User Pools indexed by their props.name */
|
|
12
|
+
private cognitoPools;
|
|
4
13
|
constructor(scope: Construct, id: string);
|
|
14
|
+
private buildRestApiGateway;
|
|
15
|
+
private buildHttpApiGateway;
|
|
16
|
+
private wireRestApiRoute;
|
|
17
|
+
private wireHttpApiRoute;
|
|
18
|
+
/**
|
|
19
|
+
* Parses a path like `/users/{id}/orders` and creates intermediate
|
|
20
|
+
* API Gateway resources, reusing already-created segments.
|
|
21
|
+
*/
|
|
22
|
+
private getOrCreateRestResource;
|
|
23
|
+
private getOrCreateAuthorizer;
|
|
24
|
+
private createCorsOptionsMethod;
|
|
5
25
|
}
|
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.FrameworkStack = void 0;
|
|
4
37
|
const cdktf_1 = require("cdktf");
|
|
@@ -16,15 +49,79 @@ const iam_role_policy_1 = require("@cdktf/provider-aws/lib/iam-role-policy");
|
|
|
16
49
|
const db_instance_1 = require("@cdktf/provider-aws/lib/db-instance");
|
|
17
50
|
const lambda_function_1 = require("@cdktf/provider-aws/lib/lambda-function");
|
|
18
51
|
const lambda_event_source_mapping_1 = require("@cdktf/provider-aws/lib/lambda-event-source-mapping");
|
|
52
|
+
const lambda_permission_1 = require("@cdktf/provider-aws/lib/lambda-permission");
|
|
53
|
+
const api_gateway_rest_api_1 = require("@cdktf/provider-aws/lib/api-gateway-rest-api");
|
|
54
|
+
const api_gateway_resource_1 = require("@cdktf/provider-aws/lib/api-gateway-resource");
|
|
55
|
+
const api_gateway_method_1 = require("@cdktf/provider-aws/lib/api-gateway-method");
|
|
56
|
+
const api_gateway_integration_1 = require("@cdktf/provider-aws/lib/api-gateway-integration");
|
|
57
|
+
const api_gateway_deployment_1 = require("@cdktf/provider-aws/lib/api-gateway-deployment");
|
|
58
|
+
const api_gateway_stage_1 = require("@cdktf/provider-aws/lib/api-gateway-stage");
|
|
59
|
+
const api_gateway_authorizer_1 = require("@cdktf/provider-aws/lib/api-gateway-authorizer");
|
|
60
|
+
const api_gateway_method_response_1 = require("@cdktf/provider-aws/lib/api-gateway-method-response");
|
|
61
|
+
const api_gateway_integration_response_1 = require("@cdktf/provider-aws/lib/api-gateway-integration-response");
|
|
62
|
+
const apigatewayv2_api_1 = require("@cdktf/provider-aws/lib/apigatewayv2-api");
|
|
63
|
+
const apigatewayv2_integration_1 = require("@cdktf/provider-aws/lib/apigatewayv2-integration");
|
|
64
|
+
const apigatewayv2_route_1 = require("@cdktf/provider-aws/lib/apigatewayv2-route");
|
|
65
|
+
const apigatewayv2_stage_1 = require("@cdktf/provider-aws/lib/apigatewayv2-stage");
|
|
19
66
|
const core_1 = require("@aetherionfw/core");
|
|
67
|
+
const path = __importStar(require("path"));
|
|
68
|
+
const fs = __importStar(require("fs"));
|
|
20
69
|
class FrameworkStack extends cdktf_1.TerraformStack {
|
|
70
|
+
/**
|
|
71
|
+
* Resolves and loads `aetherion.config.ts` from the current working directory.
|
|
72
|
+
* Falls back to safe defaults if no config file is found.
|
|
73
|
+
*/
|
|
74
|
+
static loadConfig() {
|
|
75
|
+
const configPath = path.resolve(process.cwd(), 'aetherion.config.ts');
|
|
76
|
+
const configJsPath = path.resolve(process.cwd(), 'aetherion.config.js');
|
|
77
|
+
let resolvedPath = null;
|
|
78
|
+
if (fs.existsSync(configPath))
|
|
79
|
+
resolvedPath = configPath;
|
|
80
|
+
else if (fs.existsSync(configJsPath))
|
|
81
|
+
resolvedPath = configJsPath;
|
|
82
|
+
if (!resolvedPath) {
|
|
83
|
+
console.warn('[Aetherion] No aetherion.config.ts found. Using default AWS provider settings.\n' +
|
|
84
|
+
' Run `aetherion init` to scaffold a config file, or create aetherion.config.ts manually.');
|
|
85
|
+
return {
|
|
86
|
+
accountId: process.env.AWS_ACCOUNT_ID ?? '',
|
|
87
|
+
region: process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? 'us-east-1',
|
|
88
|
+
profile: process.env.AWS_PROFILE,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
// Use require for .js, ts-node/register path for .ts
|
|
93
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
94
|
+
const mod = require(resolvedPath);
|
|
95
|
+
const config = mod.default ?? mod;
|
|
96
|
+
return {
|
|
97
|
+
accountId: process.env.AWS_ACCOUNT_ID ?? config.accountId,
|
|
98
|
+
region: process.env.AWS_REGION ?? config.region,
|
|
99
|
+
profile: process.env.AWS_PROFILE ?? config.profile,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
console.error(`[Aetherion] Failed to load aetherion.config.ts: ${err.message}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
throw new Error('unreachable'); // satisfy TypeScript return type
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** All created Lambda functions indexed by their function name */
|
|
109
|
+
lambdaFunctions = new Map();
|
|
110
|
+
/** All created Cognito User Pools indexed by their props.name */
|
|
111
|
+
cognitoPools = new Map();
|
|
21
112
|
constructor(scope, id) {
|
|
22
113
|
super(scope, id);
|
|
114
|
+
const config = FrameworkStack.loadConfig();
|
|
23
115
|
new provider_1.AwsProvider(this, 'AWS', {
|
|
24
|
-
region:
|
|
116
|
+
region: config.region,
|
|
117
|
+
...(config.profile ? { profile: config.profile } : {}),
|
|
118
|
+
...(config.accountId ? { allowedAccountIds: [config.accountId] } : {}),
|
|
25
119
|
});
|
|
26
120
|
const registry = core_1.MetadataRegistry.getInstance();
|
|
121
|
+
// ────────────────────────────────────────────
|
|
27
122
|
// 1. Build Infra Resources
|
|
123
|
+
// ────────────────────────────────────────────
|
|
124
|
+
const apiGatewayConfigs = new Map();
|
|
28
125
|
const infraClasses = registry.getInfraClasses();
|
|
29
126
|
for (const target of infraClasses) {
|
|
30
127
|
const resources = registry.getInfraResources(target);
|
|
@@ -34,7 +131,6 @@ class FrameworkStack extends cdktf_1.TerraformStack {
|
|
|
34
131
|
case 'S3Bucket':
|
|
35
132
|
new s3_bucket_1.S3Bucket(this, res.name, {
|
|
36
133
|
bucket: res.props.name,
|
|
37
|
-
// Minimal abstracted configuration mapped to raw CDKTF
|
|
38
134
|
});
|
|
39
135
|
break;
|
|
40
136
|
case 'DynamoTable':
|
|
@@ -64,7 +160,6 @@ class FrameworkStack extends cdktf_1.TerraformStack {
|
|
|
64
160
|
});
|
|
65
161
|
break;
|
|
66
162
|
case 'CloudFrontDistribution':
|
|
67
|
-
// Simplified mapping for a highly complex resource
|
|
68
163
|
new cloudfront_distribution_1.CloudfrontDistribution(this, res.name, {
|
|
69
164
|
enabled: true,
|
|
70
165
|
origin: [{
|
|
@@ -83,11 +178,13 @@ class FrameworkStack extends cdktf_1.TerraformStack {
|
|
|
83
178
|
restrictions: { geoRestriction: { restrictionType: 'none' } },
|
|
84
179
|
});
|
|
85
180
|
break;
|
|
86
|
-
case 'CognitoUserPool':
|
|
87
|
-
new cognito_user_pool_1.CognitoUserPool(this, res.name, {
|
|
181
|
+
case 'CognitoUserPool': {
|
|
182
|
+
const pool = new cognito_user_pool_1.CognitoUserPool(this, res.name, {
|
|
88
183
|
name: res.props.name,
|
|
89
184
|
});
|
|
185
|
+
this.cognitoPools.set(res.props.name, pool);
|
|
90
186
|
break;
|
|
187
|
+
}
|
|
91
188
|
case 'Vpc':
|
|
92
189
|
new vpc_1.Vpc(this, res.name, {
|
|
93
190
|
cidrBlock: res.props.cidr,
|
|
@@ -99,7 +196,7 @@ class FrameworkStack extends cdktf_1.TerraformStack {
|
|
|
99
196
|
case 'IamRole':
|
|
100
197
|
new iam_role_1.IamRole(this, res.name, {
|
|
101
198
|
name: res.props.name,
|
|
102
|
-
assumeRolePolicy: res.props.assumedBy,
|
|
199
|
+
assumeRolePolicy: res.props.assumedBy,
|
|
103
200
|
});
|
|
104
201
|
break;
|
|
105
202
|
case 'RdsInstance':
|
|
@@ -107,23 +204,27 @@ class FrameworkStack extends cdktf_1.TerraformStack {
|
|
|
107
204
|
engine: res.props.engine,
|
|
108
205
|
instanceClass: res.props.size,
|
|
109
206
|
dbName: res.props.dbName,
|
|
110
|
-
skipFinalSnapshot: true,
|
|
207
|
+
skipFinalSnapshot: true,
|
|
111
208
|
});
|
|
112
209
|
break;
|
|
210
|
+
case 'ApiGateway':
|
|
211
|
+
// Collect for later processing (after Lambdas are created)
|
|
212
|
+
apiGatewayConfigs.set(res.props.name, { infraResource: res, props: res.props });
|
|
213
|
+
break;
|
|
113
214
|
default:
|
|
114
215
|
console.warn(`Unknown infra resource type: ${res.type}`);
|
|
115
216
|
}
|
|
116
217
|
}
|
|
117
218
|
}
|
|
219
|
+
// ────────────────────────────────────────────
|
|
118
220
|
// 2. Build Lambdas and IAM Policies (1:1 Lambda per Handle Architecture)
|
|
221
|
+
// ────────────────────────────────────────────
|
|
119
222
|
const controllers = registry.getControllers();
|
|
120
223
|
for (const [target, controllerMeta] of controllers) {
|
|
121
224
|
const handles = registry.getHandles(target);
|
|
122
225
|
const iamPermissions = registry.getIamPermissions(target);
|
|
123
|
-
const routes = registry.getRoutes(target);
|
|
124
226
|
const sqsTriggers = registry.getSqsTriggers(target);
|
|
125
227
|
if (handles.length === 0) {
|
|
126
|
-
// Fallback or skip if no methods are decorated with @Handle
|
|
127
228
|
continue;
|
|
128
229
|
}
|
|
129
230
|
for (const handle of handles) {
|
|
@@ -145,7 +246,6 @@ class FrameworkStack extends cdktf_1.TerraformStack {
|
|
|
145
246
|
// 2b. Attach IAM Policies (Method overrides Class)
|
|
146
247
|
let handlePerms = iamPermissions.find(p => p.methodName === methodName);
|
|
147
248
|
if (!handlePerms) {
|
|
148
|
-
// Fallback to class-level permissions
|
|
149
249
|
handlePerms = iamPermissions.find(p => !p.methodName);
|
|
150
250
|
}
|
|
151
251
|
if (handlePerms) {
|
|
@@ -178,26 +278,344 @@ class FrameworkStack extends cdktf_1.TerraformStack {
|
|
|
178
278
|
memorySize: handle.memorySize || controllerMeta.memorySize || 128,
|
|
179
279
|
timeout: handle.timeout || controllerMeta.timeout || 3,
|
|
180
280
|
role: role.arn,
|
|
181
|
-
filename: 'dummy.zip',
|
|
281
|
+
filename: 'dummy.zip',
|
|
182
282
|
handler: 'index.handler',
|
|
183
283
|
environment: {
|
|
184
284
|
variables: {
|
|
185
|
-
AETHERION_TARGET_CLASS: controllerMeta.lambdaName,
|
|
285
|
+
AETHERION_TARGET_CLASS: controllerMeta.lambdaName,
|
|
186
286
|
AETHERION_TARGET_METHOD: methodName,
|
|
187
287
|
}
|
|
188
288
|
}
|
|
189
289
|
});
|
|
290
|
+
this.lambdaFunctions.set(lambdaName, lambdaFunction);
|
|
190
291
|
// 2d. Check for SQS Triggers targeting this handle
|
|
191
292
|
const handleTriggers = sqsTriggers.filter(t => t.methodName === methodName);
|
|
192
293
|
for (const trigger of handleTriggers) {
|
|
193
294
|
console.log(`Linking SQS Trigger ${trigger.queueName} to ${lambdaName}`);
|
|
194
295
|
new lambda_event_source_mapping_1.LambdaEventSourceMapping(this, `${lambdaName}-${trigger.queueName}-mapping`, {
|
|
195
296
|
functionName: lambdaFunction.arn,
|
|
196
|
-
eventSourceArn: `arn:aws:sqs:us-east-1:123456789012:${trigger.queueName}`,
|
|
297
|
+
eventSourceArn: `arn:aws:sqs:us-east-1:123456789012:${trigger.queueName}`,
|
|
197
298
|
});
|
|
198
299
|
}
|
|
199
300
|
}
|
|
200
301
|
}
|
|
302
|
+
// ────────────────────────────────────────────
|
|
303
|
+
// 3. Build API Gateways and wire Routes
|
|
304
|
+
// ────────────────────────────────────────────
|
|
305
|
+
const apiRefs = new Map();
|
|
306
|
+
// 3a. Create or import each API Gateway
|
|
307
|
+
for (const [apiName, config] of apiGatewayConfigs) {
|
|
308
|
+
const props = config.props;
|
|
309
|
+
console.log(`Building API Gateway: ${apiName} (${props.type})`);
|
|
310
|
+
if (props.type === 'REST') {
|
|
311
|
+
this.buildRestApiGateway(apiName, props, apiRefs);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
this.buildHttpApiGateway(apiName, props, apiRefs);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// 3b. Wire controllers to their API Gateways
|
|
318
|
+
for (const [target, controllerMeta] of controllers) {
|
|
319
|
+
if (!controllerMeta.apiGateway)
|
|
320
|
+
continue;
|
|
321
|
+
const apiRef = apiRefs.get(controllerMeta.apiGateway);
|
|
322
|
+
if (!apiRef) {
|
|
323
|
+
console.warn(`API Gateway "${controllerMeta.apiGateway}" not found for controller "${controllerMeta.lambdaName}"`);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const routes = registry.getRoutes(target);
|
|
327
|
+
const handles = registry.getHandles(target);
|
|
328
|
+
for (const route of routes) {
|
|
329
|
+
// Find the matching handle for this route to get the lambda name
|
|
330
|
+
const handle = handles.find(h => h.methodName === route.methodName);
|
|
331
|
+
if (!handle)
|
|
332
|
+
continue;
|
|
333
|
+
const lambdaName = `${controllerMeta.lambdaName}-${route.methodName}`;
|
|
334
|
+
const lambdaFn = this.lambdaFunctions.get(lambdaName);
|
|
335
|
+
if (!lambdaFn)
|
|
336
|
+
continue;
|
|
337
|
+
console.log(`Wiring ${route.method} ${route.path} → ${lambdaName}`);
|
|
338
|
+
if (apiRef.metadata.type === 'REST') {
|
|
339
|
+
this.wireRestApiRoute(apiRef, route, lambdaFn, lambdaName);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
this.wireHttpApiRoute(apiRef, route, lambdaFn, lambdaName);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// 3c. Create Deployments and Stages
|
|
347
|
+
for (const [apiName, apiRef] of apiRefs) {
|
|
348
|
+
const stageName = apiRef.metadata.stageName || 'dev';
|
|
349
|
+
if (apiRef.metadata.type === 'REST' && apiRef.restApi && !apiRef.metadata.existingApiId) {
|
|
350
|
+
const deployment = new api_gateway_deployment_1.ApiGatewayDeployment(this, `${apiName}-deployment`, {
|
|
351
|
+
restApiId: apiRef.restApi.id,
|
|
352
|
+
lifecycle: {
|
|
353
|
+
createBeforeDestroy: true,
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
new api_gateway_stage_1.ApiGatewayStage(this, `${apiName}-stage`, {
|
|
357
|
+
restApiId: apiRef.restApi.id,
|
|
358
|
+
deploymentId: deployment.id,
|
|
359
|
+
stageName,
|
|
360
|
+
});
|
|
361
|
+
console.log(`Created REST API deployment: ${apiName} → stage "${stageName}"`);
|
|
362
|
+
}
|
|
363
|
+
else if (apiRef.metadata.type === 'HTTP' && apiRef.httpApi) {
|
|
364
|
+
new apigatewayv2_stage_1.Apigatewayv2Stage(this, `${apiName}-stage`, {
|
|
365
|
+
apiId: apiRef.httpApi.id,
|
|
366
|
+
name: stageName,
|
|
367
|
+
autoDeploy: true,
|
|
368
|
+
});
|
|
369
|
+
console.log(`Created HTTP API stage: ${apiName} → "${stageName}"`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// ────────────────────────────────────────────
|
|
374
|
+
// REST API Gateway Builder
|
|
375
|
+
// ────────────────────────────────────────────
|
|
376
|
+
buildRestApiGateway(apiName, props, apiRefs) {
|
|
377
|
+
if (props.existingApiId) {
|
|
378
|
+
// Import existing REST API
|
|
379
|
+
console.log(`Importing existing REST API: ${props.existingApiId}`);
|
|
380
|
+
apiRefs.set(apiName, {
|
|
381
|
+
metadata: props,
|
|
382
|
+
rootResourceId: props.existingRootResourceId || '',
|
|
383
|
+
resourceMap: new Map(),
|
|
384
|
+
authorizerMap: new Map(),
|
|
385
|
+
methodIds: [],
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
// Create new REST API
|
|
390
|
+
const restApi = new api_gateway_rest_api_1.ApiGatewayRestApi(this, apiName, {
|
|
391
|
+
name: props.name,
|
|
392
|
+
description: props.description || `API Gateway for ${props.name}`,
|
|
393
|
+
});
|
|
394
|
+
apiRefs.set(apiName, {
|
|
395
|
+
metadata: props,
|
|
396
|
+
restApi,
|
|
397
|
+
rootResourceId: restApi.rootResourceId,
|
|
398
|
+
resourceMap: new Map(),
|
|
399
|
+
authorizerMap: new Map(),
|
|
400
|
+
methodIds: [],
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// ────────────────────────────────────────────
|
|
405
|
+
// HTTP API Gateway Builder
|
|
406
|
+
// ────────────────────────────────────────────
|
|
407
|
+
buildHttpApiGateway(apiName, props, apiRefs) {
|
|
408
|
+
if (props.existingApiId) {
|
|
409
|
+
console.log(`Importing existing HTTP API: ${props.existingApiId}`);
|
|
410
|
+
apiRefs.set(apiName, {
|
|
411
|
+
metadata: props,
|
|
412
|
+
rootResourceId: '',
|
|
413
|
+
resourceMap: new Map(),
|
|
414
|
+
authorizerMap: new Map(),
|
|
415
|
+
methodIds: [],
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
const corsConfig = props.corsEnabled !== false ? {
|
|
420
|
+
allowOrigins: props.corsOrigins || ['*'],
|
|
421
|
+
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
422
|
+
allowHeaders: ['Content-Type', 'Authorization', 'X-Amz-Date', 'X-Api-Key'],
|
|
423
|
+
} : undefined;
|
|
424
|
+
const httpApi = new apigatewayv2_api_1.Apigatewayv2Api(this, apiName, {
|
|
425
|
+
name: props.name,
|
|
426
|
+
protocolType: 'HTTP',
|
|
427
|
+
description: props.description || `HTTP API for ${props.name}`,
|
|
428
|
+
corsConfiguration: corsConfig,
|
|
429
|
+
});
|
|
430
|
+
apiRefs.set(apiName, {
|
|
431
|
+
metadata: props,
|
|
432
|
+
httpApi,
|
|
433
|
+
rootResourceId: '',
|
|
434
|
+
resourceMap: new Map(),
|
|
435
|
+
authorizerMap: new Map(),
|
|
436
|
+
methodIds: [],
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// ────────────────────────────────────────────
|
|
441
|
+
// REST API Route Wiring
|
|
442
|
+
// ────────────────────────────────────────────
|
|
443
|
+
wireRestApiRoute(apiRef, route, lambdaFn, lambdaName) {
|
|
444
|
+
const restApi = apiRef.restApi;
|
|
445
|
+
const restApiId = restApi ? restApi.id : apiRef.metadata.existingApiId;
|
|
446
|
+
// Build hierarchical resources for the path
|
|
447
|
+
const resourceId = this.getOrCreateRestResource(apiRef, route.path, restApiId);
|
|
448
|
+
// Determine authorization type
|
|
449
|
+
let authorizationType = 'NONE';
|
|
450
|
+
let authorizerId;
|
|
451
|
+
if (route.authorizer) {
|
|
452
|
+
const authorizer = this.getOrCreateAuthorizer(apiRef, route.authorizer, restApiId);
|
|
453
|
+
if (authorizer) {
|
|
454
|
+
authorizationType = 'COGNITO_USER_POOLS';
|
|
455
|
+
authorizerId = authorizer.id;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const methodId = `${lambdaName}-${route.method}`;
|
|
459
|
+
// Create Method
|
|
460
|
+
const method = new api_gateway_method_1.ApiGatewayMethod(this, methodId, {
|
|
461
|
+
restApiId,
|
|
462
|
+
resourceId,
|
|
463
|
+
httpMethod: route.method.toUpperCase(),
|
|
464
|
+
authorization: authorizationType,
|
|
465
|
+
authorizerId,
|
|
466
|
+
});
|
|
467
|
+
apiRef.methodIds.push(methodId);
|
|
468
|
+
// Create Integration (AWS_PROXY)
|
|
469
|
+
new api_gateway_integration_1.ApiGatewayIntegration(this, `${methodId}-integration`, {
|
|
470
|
+
restApiId,
|
|
471
|
+
resourceId,
|
|
472
|
+
httpMethod: method.httpMethod,
|
|
473
|
+
type: 'AWS_PROXY',
|
|
474
|
+
integrationHttpMethod: 'POST',
|
|
475
|
+
uri: lambdaFn.invokeArn,
|
|
476
|
+
});
|
|
477
|
+
// Grant API Gateway permission to invoke Lambda
|
|
478
|
+
new lambda_permission_1.LambdaPermission(this, `${methodId}-permission`, {
|
|
479
|
+
statementId: `AllowAPIGateway-${methodId}`,
|
|
480
|
+
action: 'lambda:InvokeFunction',
|
|
481
|
+
functionName: lambdaFn.functionName,
|
|
482
|
+
principal: 'apigateway.amazonaws.com',
|
|
483
|
+
});
|
|
484
|
+
// CORS: Create OPTIONS method if CORS is enabled
|
|
485
|
+
if (apiRef.metadata.corsEnabled !== false) {
|
|
486
|
+
this.createCorsOptionsMethod(apiRef, route.path, restApiId, resourceId, lambdaName);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// ────────────────────────────────────────────
|
|
490
|
+
// HTTP API Route Wiring
|
|
491
|
+
// ────────────────────────────────────────────
|
|
492
|
+
wireHttpApiRoute(apiRef, route, lambdaFn, lambdaName) {
|
|
493
|
+
const httpApi = apiRef.httpApi;
|
|
494
|
+
const apiId = httpApi ? httpApi.id : apiRef.metadata.existingApiId;
|
|
495
|
+
const integrationId = `${lambdaName}-${route.method}-int`;
|
|
496
|
+
// Create Integration
|
|
497
|
+
const integration = new apigatewayv2_integration_1.Apigatewayv2Integration(this, integrationId, {
|
|
498
|
+
apiId,
|
|
499
|
+
integrationType: 'AWS_PROXY',
|
|
500
|
+
integrationUri: lambdaFn.invokeArn,
|
|
501
|
+
payloadFormatVersion: '2.0',
|
|
502
|
+
});
|
|
503
|
+
// Create Route
|
|
504
|
+
const routeKey = `${route.method.toUpperCase()} ${route.path}`;
|
|
505
|
+
new apigatewayv2_route_1.Apigatewayv2Route(this, `${lambdaName}-${route.method}-route`, {
|
|
506
|
+
apiId,
|
|
507
|
+
routeKey,
|
|
508
|
+
target: `integrations/${integration.id}`,
|
|
509
|
+
});
|
|
510
|
+
// Grant API Gateway permission to invoke Lambda
|
|
511
|
+
new lambda_permission_1.LambdaPermission(this, `${lambdaName}-${route.method}-permission`, {
|
|
512
|
+
statementId: `AllowHTTPAPI-${lambdaName}-${route.method}`,
|
|
513
|
+
action: 'lambda:InvokeFunction',
|
|
514
|
+
functionName: lambdaFn.functionName,
|
|
515
|
+
principal: 'apigateway.amazonaws.com',
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
// ────────────────────────────────────────────
|
|
519
|
+
// Hierarchical Resource Builder (REST API)
|
|
520
|
+
// ────────────────────────────────────────────
|
|
521
|
+
/**
|
|
522
|
+
* Parses a path like `/users/{id}/orders` and creates intermediate
|
|
523
|
+
* API Gateway resources, reusing already-created segments.
|
|
524
|
+
*/
|
|
525
|
+
getOrCreateRestResource(apiRef, path, restApiId) {
|
|
526
|
+
if (path === '/')
|
|
527
|
+
return apiRef.rootResourceId;
|
|
528
|
+
const segments = path.split('/').filter(Boolean);
|
|
529
|
+
let currentParentId = apiRef.rootResourceId;
|
|
530
|
+
let currentPath = '';
|
|
531
|
+
for (const segment of segments) {
|
|
532
|
+
currentPath += `/${segment}`;
|
|
533
|
+
if (apiRef.resourceMap.has(currentPath)) {
|
|
534
|
+
currentParentId = apiRef.resourceMap.get(currentPath).id;
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const resource = new api_gateway_resource_1.ApiGatewayResource(this, `resource-${currentPath.replace(/[/{}]/g, '-')}`, {
|
|
538
|
+
restApiId,
|
|
539
|
+
parentId: currentParentId,
|
|
540
|
+
pathPart: segment,
|
|
541
|
+
});
|
|
542
|
+
apiRef.resourceMap.set(currentPath, resource);
|
|
543
|
+
currentParentId = resource.id;
|
|
544
|
+
}
|
|
545
|
+
return currentParentId;
|
|
546
|
+
}
|
|
547
|
+
// ────────────────────────────────────────────
|
|
548
|
+
// Cognito Authorizer Builder (REST API)
|
|
549
|
+
// ────────────────────────────────────────────
|
|
550
|
+
getOrCreateAuthorizer(apiRef, authorizerName, restApiId) {
|
|
551
|
+
// Reuse existing authorizer if already created for this API
|
|
552
|
+
if (apiRef.authorizerMap.has(authorizerName)) {
|
|
553
|
+
return apiRef.authorizerMap.get(authorizerName);
|
|
554
|
+
}
|
|
555
|
+
// Look up the Cognito User Pool by name
|
|
556
|
+
const pool = this.cognitoPools.get(authorizerName);
|
|
557
|
+
if (!pool) {
|
|
558
|
+
console.warn(`Authorizer "${authorizerName}" references a Cognito User Pool that was not found in @Infra resources.`);
|
|
559
|
+
return undefined;
|
|
560
|
+
}
|
|
561
|
+
const authorizer = new api_gateway_authorizer_1.ApiGatewayAuthorizer(this, `${authorizerName}-authorizer`, {
|
|
562
|
+
name: `${authorizerName}-cognito-authorizer`,
|
|
563
|
+
restApiId,
|
|
564
|
+
type: 'COGNITO_USER_POOLS',
|
|
565
|
+
providerArns: [pool.arn],
|
|
566
|
+
});
|
|
567
|
+
apiRef.authorizerMap.set(authorizerName, authorizer);
|
|
568
|
+
return authorizer;
|
|
569
|
+
}
|
|
570
|
+
// ────────────────────────────────────────────
|
|
571
|
+
// CORS OPTIONS Method (REST API)
|
|
572
|
+
// ────────────────────────────────────────────
|
|
573
|
+
createCorsOptionsMethod(apiRef, path, restApiId, resourceId, lambdaName) {
|
|
574
|
+
const corsId = `${lambdaName}-OPTIONS-${path.replace(/[/{}]/g, '-')}`;
|
|
575
|
+
// Avoid duplicate OPTIONS methods on the same resource
|
|
576
|
+
if (apiRef.methodIds.includes(corsId))
|
|
577
|
+
return;
|
|
578
|
+
apiRef.methodIds.push(corsId);
|
|
579
|
+
const origins = apiRef.metadata.corsOrigins?.join(',') || '*';
|
|
580
|
+
const headers = 'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token';
|
|
581
|
+
const methods = 'GET,POST,PUT,PATCH,DELETE,OPTIONS';
|
|
582
|
+
const optionsMethod = new api_gateway_method_1.ApiGatewayMethod(this, corsId, {
|
|
583
|
+
restApiId,
|
|
584
|
+
resourceId,
|
|
585
|
+
httpMethod: 'OPTIONS',
|
|
586
|
+
authorization: 'NONE',
|
|
587
|
+
});
|
|
588
|
+
new api_gateway_integration_1.ApiGatewayIntegration(this, `${corsId}-integration`, {
|
|
589
|
+
restApiId,
|
|
590
|
+
resourceId,
|
|
591
|
+
httpMethod: optionsMethod.httpMethod,
|
|
592
|
+
type: 'MOCK',
|
|
593
|
+
requestTemplates: {
|
|
594
|
+
'application/json': '{"statusCode": 200}',
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
new api_gateway_method_response_1.ApiGatewayMethodResponse(this, `${corsId}-response`, {
|
|
598
|
+
restApiId,
|
|
599
|
+
resourceId,
|
|
600
|
+
httpMethod: optionsMethod.httpMethod,
|
|
601
|
+
statusCode: '200',
|
|
602
|
+
responseParameters: {
|
|
603
|
+
'method.response.header.Access-Control-Allow-Headers': true,
|
|
604
|
+
'method.response.header.Access-Control-Allow-Methods': true,
|
|
605
|
+
'method.response.header.Access-Control-Allow-Origin': true,
|
|
606
|
+
},
|
|
607
|
+
});
|
|
608
|
+
new api_gateway_integration_response_1.ApiGatewayIntegrationResponse(this, `${corsId}-int-response`, {
|
|
609
|
+
restApiId,
|
|
610
|
+
resourceId,
|
|
611
|
+
httpMethod: optionsMethod.httpMethod,
|
|
612
|
+
statusCode: '200',
|
|
613
|
+
responseParameters: {
|
|
614
|
+
'method.response.header.Access-Control-Allow-Headers': `'${headers}'`,
|
|
615
|
+
'method.response.header.Access-Control-Allow-Methods': `'${methods}'`,
|
|
616
|
+
'method.response.header.Access-Control-Allow-Origin': `'${origins}'`,
|
|
617
|
+
},
|
|
618
|
+
});
|
|
201
619
|
}
|
|
202
620
|
}
|
|
203
621
|
exports.FrameworkStack = FrameworkStack;
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aetherionfw/infra",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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.1"
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|
|
13
|
+
"@types/node": "^20.0.0",
|
|
13
14
|
"typescript": "^5.5.4"
|
|
14
15
|
},
|
|
15
16
|
"publishConfig": {
|
|
@@ -22,6 +23,11 @@
|
|
|
22
23
|
],
|
|
23
24
|
"author": "CrisD3v",
|
|
24
25
|
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/CrisD3v/Aetherion-framework.git",
|
|
29
|
+
"directory": "packages/infra"
|
|
30
|
+
},
|
|
25
31
|
"scripts": {
|
|
26
32
|
"build": "tsc",
|
|
27
33
|
"synth": "cdktf synth",
|