@geek-fun/serverlessinsight 0.3.4 → 0.4.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 +209 -21
- package/README.zh-CN.md +232 -0
- package/dist/package.json +33 -33
- package/dist/src/commands/index.js +50 -12
- package/dist/src/commands/local.js +6 -3
- package/dist/src/commands/template.js +3 -1
- package/dist/src/common/constants.js +3 -1
- package/dist/src/common/context.js +56 -30
- package/dist/src/common/credentials.js +15 -0
- package/dist/src/common/iacHelper.js +39 -4
- package/dist/src/common/index.d.ts +2 -1
- package/dist/src/common/index.js +2 -1
- package/dist/src/common/logger.js +6 -0
- package/dist/src/common/requestHelper.js +16 -0
- package/dist/src/common/rosClient.js +3 -0
- package/dist/src/parser/eventParser.js +1 -1
- package/dist/src/parser/index.d.ts +2 -1
- package/dist/src/parser/index.js +32 -1
- package/dist/src/stack/localStack/aliyunFc.js +145 -0
- package/dist/src/stack/localStack/bucket.js +226 -0
- package/dist/src/stack/localStack/event.js +133 -26
- package/dist/src/stack/localStack/function.js +120 -0
- package/dist/src/stack/localStack/functionRunner.js +270 -0
- package/dist/src/stack/localStack/index.d.ts +4 -1
- package/dist/src/stack/localStack/index.js +14 -4
- package/dist/src/stack/localStack/localServer.js +111 -0
- package/dist/src/stack/localStack/utils.js +36 -0
- package/dist/src/stack/rosStack/bootstrap.js +1 -1
- package/dist/src/types/localStack/index.d.ts +81 -0
- package/dist/src/types/localStack/index.js +10 -0
- package/dist/src/validator/iacSchema.js +17 -2
- package/dist/src/validator/rootSchema.js +46 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/layers/si-bootstrap-sdk/README.md +63 -0
- package/layers/si-bootstrap-sdk/package-lock.json +39 -33
- package/layers/si-bootstrap-sdk/package.json +5 -5
- package/layers/si-bootstrap-sdk/support/operation-collection/README.md +47 -0
- package/layers/si-bootstrap-sdk/support/operation-collection/package-lock.json +298 -0
- package/layers/si-bootstrap-sdk/support/operation-collection/package.json +18 -0
- package/layers/si-bootstrap-sdk/support/operation-collection/publish.js +257 -0
- package/package.json +33 -33
- package/samples/aliyun-poc-es.yml +16 -12
- package/dist/src/common/domainHelper.js +0 -10
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateRequestId = exports.logApiGatewayRequest = exports.transformFCResponse = exports.addFCHeaders = exports.createAliyunContext = exports.createAliyunContextSerializable = exports.transformToAliyunEvent = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const common_1 = require("../../common");
|
|
6
|
+
const createFCLogger = (requestId) => {
|
|
7
|
+
const formatLog = (level, message) => {
|
|
8
|
+
const timestamp = new Date().toISOString();
|
|
9
|
+
console.log(`${timestamp} ${requestId} [${level}] ${message}`);
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
debug: (message) => formatLog('DEBUG', message),
|
|
13
|
+
info: (message) => formatLog('INFO', message),
|
|
14
|
+
warn: (message) => formatLog('WARNING', message),
|
|
15
|
+
error: (message) => formatLog('ERROR', message),
|
|
16
|
+
log: (message) => formatLog('INFO', message),
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
const transformToAliyunEvent = async (req, url, query) => {
|
|
20
|
+
const rawBody = await (0, common_1.readRequestBody)(req);
|
|
21
|
+
const pathParameters = {};
|
|
22
|
+
const event = {
|
|
23
|
+
path: url,
|
|
24
|
+
httpMethod: req.method || 'GET',
|
|
25
|
+
headers: req.headers,
|
|
26
|
+
queryParameters: query,
|
|
27
|
+
pathParameters,
|
|
28
|
+
body: rawBody || undefined,
|
|
29
|
+
isBase64Encoded: false,
|
|
30
|
+
};
|
|
31
|
+
const eventBuffer = Buffer.from(JSON.stringify(event));
|
|
32
|
+
return { event: eventBuffer, headers: req.headers };
|
|
33
|
+
};
|
|
34
|
+
exports.transformToAliyunEvent = transformToAliyunEvent;
|
|
35
|
+
const createAliyunContextSerializable = (iac, functionName, handler, memory, timeout, requestId) => {
|
|
36
|
+
return {
|
|
37
|
+
requestId,
|
|
38
|
+
region: iac.provider.region || 'cn-hangzhou',
|
|
39
|
+
accountId: process.env.ALIYUN_ACCOUNT_ID || '000000000000',
|
|
40
|
+
credentials: {
|
|
41
|
+
accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID || 'mock-access-key-id',
|
|
42
|
+
accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET || 'mock-access-key-secret',
|
|
43
|
+
securityToken: process.env.ALIYUN_SECURITY_TOKEN || '',
|
|
44
|
+
},
|
|
45
|
+
function: {
|
|
46
|
+
name: functionName,
|
|
47
|
+
handler,
|
|
48
|
+
memory,
|
|
49
|
+
timeout,
|
|
50
|
+
initializer: '',
|
|
51
|
+
},
|
|
52
|
+
service: {
|
|
53
|
+
name: iac.service || 'default-service',
|
|
54
|
+
logProject: `${iac.service}-log-project`,
|
|
55
|
+
logStore: `${iac.service}-log-store`,
|
|
56
|
+
qualifier: 'LATEST',
|
|
57
|
+
versionId: '1',
|
|
58
|
+
},
|
|
59
|
+
tracing: {
|
|
60
|
+
spanContext: '',
|
|
61
|
+
jaegerEndpoint: '',
|
|
62
|
+
spanBaggages: {},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
exports.createAliyunContextSerializable = createAliyunContextSerializable;
|
|
67
|
+
const createAliyunContext = (iac, functionName, handler, memory, timeout, requestId) => {
|
|
68
|
+
const baseContext = (0, exports.createAliyunContextSerializable)(iac, functionName, handler, memory, timeout, requestId);
|
|
69
|
+
return {
|
|
70
|
+
...baseContext,
|
|
71
|
+
tracing: {
|
|
72
|
+
...baseContext.tracing,
|
|
73
|
+
parseOpenTracingBaggages: () => ({}),
|
|
74
|
+
},
|
|
75
|
+
logger: createFCLogger(requestId),
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
exports.createAliyunContext = createAliyunContext;
|
|
79
|
+
const addFCHeaders = (context, headers) => {
|
|
80
|
+
return {
|
|
81
|
+
...headers,
|
|
82
|
+
'x-fc-request-id': context.requestId,
|
|
83
|
+
'x-fc-access-key-id': context.credentials.accessKeyId,
|
|
84
|
+
'x-fc-access-key-secret': context.credentials.accessKeySecret,
|
|
85
|
+
'x-fc-security-token': context.credentials.securityToken,
|
|
86
|
+
'x-fc-function-handler': context.function.handler,
|
|
87
|
+
'x-fc-function-memory': String(context.function.memory),
|
|
88
|
+
'x-fc-region': context.region,
|
|
89
|
+
'x-fc-account-id': context.accountId,
|
|
90
|
+
'x-fc-qualifier': context.service.qualifier,
|
|
91
|
+
'x-fc-version-id': context.service.versionId,
|
|
92
|
+
'x-fc-function-name': context.function.name,
|
|
93
|
+
'x-fc-service-logproject': context.service.logProject,
|
|
94
|
+
'x-fc-service-logstore': context.service.logStore,
|
|
95
|
+
'x-fc-control-path': '/http-invoke',
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
exports.addFCHeaders = addFCHeaders;
|
|
99
|
+
const transformFCResponse = (result) => {
|
|
100
|
+
if (result && typeof result === 'object' && 'statusCode' in result && 'body' in result) {
|
|
101
|
+
const { statusCode: rawStatus = 200, body: rawBody, isBase64Encoded, headers = {}, } = result;
|
|
102
|
+
const parsedStatus = typeof rawStatus === 'string' ? parseInt(rawStatus, 10) : rawStatus;
|
|
103
|
+
const statusCode = isNaN(parsedStatus) ? 200 : parsedStatus;
|
|
104
|
+
let body = rawBody;
|
|
105
|
+
if (isBase64Encoded && typeof body === 'string') {
|
|
106
|
+
body = Buffer.from(body, 'base64').toString('utf-8');
|
|
107
|
+
}
|
|
108
|
+
if (typeof body === 'string') {
|
|
109
|
+
try {
|
|
110
|
+
body = JSON.parse(body);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// If parsing fails, keep as string
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { statusCode, headers, body };
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
statusCode: 200,
|
|
120
|
+
headers: { 'Content-Type': 'application/json' },
|
|
121
|
+
body: result,
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
exports.transformFCResponse = transformFCResponse;
|
|
125
|
+
const logApiGatewayRequest = (requestId, apiPath, statusCode, startTime, endTime, sourceIp) => {
|
|
126
|
+
const duration = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(1);
|
|
127
|
+
const startTimeStr = formatDateTime(startTime);
|
|
128
|
+
const endTimeStr = formatDateTime(endTime);
|
|
129
|
+
const timestamp = formatDateTime(new Date());
|
|
130
|
+
console.log(`${timestamp} | ${requestId} | ${apiPath} | Sync Call | local-app | Development | local-project | ${statusCode} | ${startTimeStr} | ${endTimeStr} | ${duration}s | - | ${sourceIp}`);
|
|
131
|
+
};
|
|
132
|
+
exports.logApiGatewayRequest = logApiGatewayRequest;
|
|
133
|
+
const formatDateTime = (date) => {
|
|
134
|
+
const year = date.getFullYear();
|
|
135
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
136
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
137
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
138
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
139
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
140
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
141
|
+
};
|
|
142
|
+
const generateRequestId = () => {
|
|
143
|
+
return (0, crypto_1.randomUUID)().replace(/-/g, '');
|
|
144
|
+
};
|
|
145
|
+
exports.generateRequestId = generateRequestId;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.bucketsHandler = void 0;
|
|
7
|
+
const common_1 = require("../../common");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const TEXT_MIME_TYPES = new Set([
|
|
11
|
+
'text/plain',
|
|
12
|
+
'text/html',
|
|
13
|
+
'text/css',
|
|
14
|
+
'text/javascript',
|
|
15
|
+
'text/markdown',
|
|
16
|
+
'text/xml',
|
|
17
|
+
'application/json',
|
|
18
|
+
'application/javascript',
|
|
19
|
+
'application/xml',
|
|
20
|
+
]);
|
|
21
|
+
const getMimeType = (filename) => {
|
|
22
|
+
const ext = node_path_1.default.extname(filename).toLowerCase();
|
|
23
|
+
const mimeTypes = {
|
|
24
|
+
'.html': 'text/html',
|
|
25
|
+
'.htm': 'text/html',
|
|
26
|
+
'.css': 'text/css',
|
|
27
|
+
'.js': 'application/javascript',
|
|
28
|
+
'.json': 'application/json',
|
|
29
|
+
'.xml': 'application/xml',
|
|
30
|
+
'.txt': 'text/plain',
|
|
31
|
+
'.md': 'text/markdown',
|
|
32
|
+
'.jpg': 'image/jpeg',
|
|
33
|
+
'.jpeg': 'image/jpeg',
|
|
34
|
+
'.png': 'image/png',
|
|
35
|
+
'.gif': 'image/gif',
|
|
36
|
+
'.svg': 'image/svg+xml',
|
|
37
|
+
'.ico': 'image/x-icon',
|
|
38
|
+
'.pdf': 'application/pdf',
|
|
39
|
+
'.zip': 'application/zip',
|
|
40
|
+
'.tar': 'application/x-tar',
|
|
41
|
+
'.gz': 'application/gzip',
|
|
42
|
+
};
|
|
43
|
+
return mimeTypes[ext] || 'application/octet-stream';
|
|
44
|
+
};
|
|
45
|
+
const listDirectory = (dirPath, bucketPath) => {
|
|
46
|
+
try {
|
|
47
|
+
const entries = node_fs_1.default.readdirSync(dirPath, { withFileTypes: true });
|
|
48
|
+
return entries.map((entry) => {
|
|
49
|
+
const fullPath = node_path_1.default.join(dirPath, entry.name);
|
|
50
|
+
const relativePath = node_path_1.default.relative(bucketPath, fullPath);
|
|
51
|
+
const stats = node_fs_1.default.statSync(fullPath);
|
|
52
|
+
return {
|
|
53
|
+
name: entry.name,
|
|
54
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
55
|
+
size: entry.isDirectory() ? 0 : stats.size,
|
|
56
|
+
path: relativePath.replace(/\\/g, '/'), // Normalize path separators
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
common_1.logger.error(`Error listing directory: ${error}`);
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const getAllFiles = (dirPath, bucketPath, fileList = []) => {
|
|
66
|
+
try {
|
|
67
|
+
const entries = node_fs_1.default.readdirSync(dirPath, { withFileTypes: true });
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const fullPath = node_path_1.default.join(dirPath, entry.name);
|
|
70
|
+
const relativePath = node_path_1.default.relative(bucketPath, fullPath);
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
// Recursively list files in subdirectories
|
|
73
|
+
getAllFiles(fullPath, bucketPath, fileList);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const stats = node_fs_1.default.statSync(fullPath);
|
|
77
|
+
fileList.push({
|
|
78
|
+
name: entry.name,
|
|
79
|
+
type: 'file',
|
|
80
|
+
size: stats.size,
|
|
81
|
+
path: relativePath.replace(/\\/g, '/'),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return fileList;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
common_1.logger.error(`Error getting all files: ${error}`);
|
|
89
|
+
return fileList;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const bucketsHandler = async (req, parsed, iac) => {
|
|
93
|
+
common_1.logger.info(`Bucket request received by local server -> ${req.method} ${parsed.identifier ?? '/'} ${parsed.url}`);
|
|
94
|
+
// Find the bucket definition
|
|
95
|
+
const bucketDef = iac.buckets?.find((bucket) => bucket.key === parsed.identifier);
|
|
96
|
+
if (!bucketDef) {
|
|
97
|
+
return {
|
|
98
|
+
statusCode: 404,
|
|
99
|
+
body: { error: 'Bucket not found', bucketKey: parsed.identifier },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// Determine the bucket path - use website.code if available, otherwise use bucket name
|
|
103
|
+
let bucketBasePath;
|
|
104
|
+
if (bucketDef.website?.code) {
|
|
105
|
+
bucketBasePath = node_path_1.default.resolve(process.cwd(), bucketDef.website.code);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// Fallback: create a directory based on bucket name (for non-website buckets)
|
|
109
|
+
bucketBasePath = node_path_1.default.resolve(process.cwd(), bucketDef.name);
|
|
110
|
+
}
|
|
111
|
+
// Check if bucket path exists
|
|
112
|
+
if (!node_fs_1.default.existsSync(bucketBasePath)) {
|
|
113
|
+
return {
|
|
114
|
+
statusCode: 404,
|
|
115
|
+
body: {
|
|
116
|
+
error: 'Bucket directory not found',
|
|
117
|
+
bucketKey: bucketDef.key,
|
|
118
|
+
bucketName: bucketDef.name,
|
|
119
|
+
expectedPath: bucketBasePath,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// Root path lists all files in bucket
|
|
124
|
+
if (parsed.url === '/') {
|
|
125
|
+
try {
|
|
126
|
+
const files = getAllFiles(bucketBasePath, bucketBasePath);
|
|
127
|
+
return {
|
|
128
|
+
statusCode: 200,
|
|
129
|
+
headers: { 'Content-Type': 'application/json' },
|
|
130
|
+
body: {
|
|
131
|
+
bucket: bucketDef.name,
|
|
132
|
+
bucketKey: bucketDef.key,
|
|
133
|
+
files,
|
|
134
|
+
count: files.length,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
common_1.logger.error(`Error listing bucket files: ${error}`);
|
|
140
|
+
return {
|
|
141
|
+
statusCode: 500,
|
|
142
|
+
body: {
|
|
143
|
+
error: 'Failed to list bucket files',
|
|
144
|
+
message: error instanceof Error ? error.message : String(error),
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Otherwise, serve the requested file
|
|
150
|
+
const requestedPath = parsed.url.startsWith('/') ? parsed.url.slice(1) : parsed.url;
|
|
151
|
+
const filePath = node_path_1.default.join(bucketBasePath, requestedPath);
|
|
152
|
+
// Security check: ensure the requested file is within the bucket directory
|
|
153
|
+
const normalizedFilePath = node_path_1.default.normalize(filePath);
|
|
154
|
+
const normalizedBucketPath = node_path_1.default.normalize(bucketBasePath);
|
|
155
|
+
if (!normalizedFilePath.startsWith(normalizedBucketPath)) {
|
|
156
|
+
return {
|
|
157
|
+
statusCode: 403,
|
|
158
|
+
body: { error: 'Access denied: Path traversal attempt detected' },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
// Check if file exists
|
|
162
|
+
if (!node_fs_1.default.existsSync(filePath)) {
|
|
163
|
+
return {
|
|
164
|
+
statusCode: 404,
|
|
165
|
+
body: {
|
|
166
|
+
error: 'File not found',
|
|
167
|
+
path: requestedPath,
|
|
168
|
+
bucketKey: bucketDef.key,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
// If it's a directory, list its contents
|
|
173
|
+
const stats = node_fs_1.default.statSync(filePath);
|
|
174
|
+
if (stats.isDirectory()) {
|
|
175
|
+
try {
|
|
176
|
+
const files = listDirectory(filePath, bucketBasePath);
|
|
177
|
+
return {
|
|
178
|
+
statusCode: 200,
|
|
179
|
+
headers: { 'Content-Type': 'application/json' },
|
|
180
|
+
body: {
|
|
181
|
+
bucket: bucketDef.name,
|
|
182
|
+
bucketKey: bucketDef.key,
|
|
183
|
+
directory: requestedPath,
|
|
184
|
+
files,
|
|
185
|
+
count: files.length,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
common_1.logger.error(`Error listing directory: ${error}`);
|
|
191
|
+
return {
|
|
192
|
+
statusCode: 500,
|
|
193
|
+
body: {
|
|
194
|
+
error: 'Failed to list directory',
|
|
195
|
+
message: error instanceof Error ? error.message : String(error),
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Serve the file
|
|
201
|
+
try {
|
|
202
|
+
const fileContent = node_fs_1.default.readFileSync(filePath);
|
|
203
|
+
const mimeType = getMimeType(filePath);
|
|
204
|
+
// For text files, return as string; for binary files, return as base64
|
|
205
|
+
const isTextFile = TEXT_MIME_TYPES.has(mimeType);
|
|
206
|
+
return {
|
|
207
|
+
statusCode: 200,
|
|
208
|
+
headers: {
|
|
209
|
+
'Content-Type': mimeType,
|
|
210
|
+
'Content-Length': String(fileContent.length),
|
|
211
|
+
},
|
|
212
|
+
body: isTextFile ? fileContent.toString('utf-8') : fileContent.toString('base64'),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
common_1.logger.error(`Error reading file: ${error}`);
|
|
217
|
+
return {
|
|
218
|
+
statusCode: 500,
|
|
219
|
+
body: {
|
|
220
|
+
error: 'Failed to read file',
|
|
221
|
+
message: error instanceof Error ? error.message : String(error),
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
exports.bucketsHandler = bucketsHandler;
|
|
@@ -3,36 +3,143 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
7
|
-
const node_http_1 = __importDefault(require("node:http"));
|
|
8
|
-
const common_1 = require("../../common");
|
|
6
|
+
exports.eventsHandler = void 0;
|
|
9
7
|
const types_1 = require("../../types");
|
|
10
8
|
const lodash_1 = require("lodash");
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
const common_1 = require("../../common");
|
|
10
|
+
const function_1 = require("./function");
|
|
11
|
+
const aliyunFc_1 = require("./aliyunFc");
|
|
12
|
+
const functionRunner_1 = require("./functionRunner");
|
|
13
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
15
|
+
const utils_1 = require("./utils");
|
|
16
|
+
const matchTrigger = (req, trigger) => {
|
|
17
|
+
if (req.method !== 'ANY' && req.method !== trigger.method) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const normalize = (s) => s.replace(/^\/+|\/+$/g, '');
|
|
21
|
+
const [pathSegments, triggerSegments] = [
|
|
22
|
+
normalize(req.path).split('/'),
|
|
23
|
+
normalize(trigger.path).split('/'),
|
|
24
|
+
];
|
|
25
|
+
const hasWildcard = triggerSegments[triggerSegments.length - 1] === '*';
|
|
26
|
+
const prefixSegments = hasWildcard ? triggerSegments.slice(0, -1) : triggerSegments;
|
|
27
|
+
const minRequiredSegments = prefixSegments.length;
|
|
28
|
+
if (pathSegments.length < minRequiredSegments)
|
|
29
|
+
return false;
|
|
30
|
+
return prefixSegments.every((triggerSegment, index) => {
|
|
31
|
+
const pathSegment = pathSegments[index];
|
|
32
|
+
if (triggerSegment.startsWith('[') && triggerSegment.endsWith(']')) {
|
|
33
|
+
return pathSegment !== '';
|
|
19
34
|
}
|
|
20
|
-
|
|
21
|
-
res.end(`Invoked backend: ${matchedTrigger.backend}\n`);
|
|
22
|
-
common_1.logger.info(`API Gateway Event - ${req.method} ${req.url} -> ${matchedTrigger.backend}`);
|
|
23
|
-
});
|
|
24
|
-
const port = 3000 + Math.floor(Math.random() * 1000);
|
|
25
|
-
server.listen(port, () => {
|
|
26
|
-
common_1.logger.info(`API Gateway "${event.name}" listening on http://localhost:${port}`);
|
|
35
|
+
return triggerSegment === pathSegment;
|
|
27
36
|
});
|
|
28
37
|
};
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
const servEvent = async (req, parsed, iac) => {
|
|
39
|
+
const startTime = new Date();
|
|
40
|
+
const requestId = (0, aliyunFc_1.generateRequestId)();
|
|
41
|
+
const sourceIp = req.socket?.remoteAddress || '127.0.0.1';
|
|
42
|
+
const isAliyun = iac.provider.name === common_1.ProviderEnum.ALIYUN;
|
|
43
|
+
const event = iac.events?.find((event) => event.type === types_1.EventTypes.API_GATEWAY && event.key === parsed.identifier);
|
|
44
|
+
if ((0, lodash_1.isEmpty)(event)) {
|
|
45
|
+
return {
|
|
46
|
+
statusCode: 404,
|
|
47
|
+
body: { error: 'API Gateway event not found', event: parsed.identifier },
|
|
48
|
+
};
|
|
33
49
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
50
|
+
common_1.logger.info(`Event trigger ${JSON.stringify(event.triggers)}, req method: ${req.method}, req url${req.url}`);
|
|
51
|
+
const matchedTrigger = event.triggers.find((trigger) => matchTrigger({ method: parsed.method, path: parsed.url }, trigger));
|
|
52
|
+
if (!matchedTrigger) {
|
|
53
|
+
const endTime = new Date();
|
|
54
|
+
if (isAliyun) {
|
|
55
|
+
(0, aliyunFc_1.logApiGatewayRequest)(requestId, parsed.url, 404, startTime, endTime, sourceIp);
|
|
56
|
+
}
|
|
57
|
+
return { statusCode: 404, body: { error: 'No matching trigger found' } };
|
|
58
|
+
}
|
|
59
|
+
if (matchedTrigger.backend) {
|
|
60
|
+
const backendDef = (0, common_1.getIacDefinition)(iac, matchedTrigger.backend);
|
|
61
|
+
if (!backendDef) {
|
|
62
|
+
const endTime = new Date();
|
|
63
|
+
if (isAliyun) {
|
|
64
|
+
(0, aliyunFc_1.logApiGatewayRequest)(requestId, parsed.url, 500, startTime, endTime, sourceIp);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
statusCode: 500,
|
|
68
|
+
body: { error: 'Backend definition missing', backend: matchedTrigger.backend },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// For Aliyun, handle the function execution with proper event transformation
|
|
72
|
+
if (isAliyun && backendDef.code) {
|
|
73
|
+
let tempDir = null;
|
|
74
|
+
try {
|
|
75
|
+
const { event: aliyunEvent } = await (0, aliyunFc_1.transformToAliyunEvent)(req, parsed.url, parsed.query);
|
|
76
|
+
const codePath = node_path_1.default.resolve(process.cwd(), backendDef.code.path);
|
|
77
|
+
let codeDir;
|
|
78
|
+
if (codePath.endsWith('.zip') && node_fs_1.default.existsSync(codePath)) {
|
|
79
|
+
tempDir = await (0, utils_1.extractZipFile)(codePath);
|
|
80
|
+
codeDir = tempDir;
|
|
81
|
+
}
|
|
82
|
+
else if (node_fs_1.default.existsSync(codePath) && node_fs_1.default.statSync(codePath).isDirectory()) {
|
|
83
|
+
codeDir = codePath;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
codeDir = node_path_1.default.dirname(codePath);
|
|
87
|
+
}
|
|
88
|
+
const funOptions = {
|
|
89
|
+
codeDir,
|
|
90
|
+
functionKey: backendDef.key,
|
|
91
|
+
handler: backendDef.code.handler,
|
|
92
|
+
servicePath: '',
|
|
93
|
+
timeout: backendDef.timeout * 1000,
|
|
94
|
+
};
|
|
95
|
+
const aliyunContext = (0, aliyunFc_1.createAliyunContextSerializable)(iac, backendDef.name, backendDef.code.handler, backendDef.memory, backendDef.timeout, requestId);
|
|
96
|
+
const env = {
|
|
97
|
+
...backendDef.environment,
|
|
98
|
+
};
|
|
99
|
+
common_1.logger.debug(`Invoking FC function with Aliyun event format`);
|
|
100
|
+
const result = await (0, functionRunner_1.invokeFunction)(funOptions, env, aliyunEvent, aliyunContext);
|
|
101
|
+
const endTime = new Date();
|
|
102
|
+
const transformed = (0, aliyunFc_1.transformFCResponse)(result);
|
|
103
|
+
// Log API Gateway request
|
|
104
|
+
(0, aliyunFc_1.logApiGatewayRequest)(requestId, parsed.url, transformed.statusCode, startTime, endTime, sourceIp);
|
|
105
|
+
return {
|
|
106
|
+
statusCode: transformed.statusCode,
|
|
107
|
+
headers: transformed.headers,
|
|
108
|
+
body: transformed.body,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
const endTime = new Date();
|
|
113
|
+
(0, aliyunFc_1.logApiGatewayRequest)(requestId, parsed.url, 500, startTime, endTime, sourceIp);
|
|
114
|
+
common_1.logger.error(`Function execution error: ${error}`);
|
|
115
|
+
return {
|
|
116
|
+
statusCode: 500,
|
|
117
|
+
body: {
|
|
118
|
+
error: 'Function execution failed',
|
|
119
|
+
message: error instanceof Error ? error.message : String(error),
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
if (tempDir && node_fs_1.default.existsSync(tempDir)) {
|
|
125
|
+
node_fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// For non-Aliyun or when using functionsHandler
|
|
130
|
+
const result = await (0, function_1.functionsHandler)(req, { ...parsed, identifier: backendDef?.key }, iac);
|
|
131
|
+
const endTime = new Date();
|
|
132
|
+
if (isAliyun) {
|
|
133
|
+
(0, aliyunFc_1.logApiGatewayRequest)(requestId, parsed.url, result.statusCode, startTime, endTime, sourceIp);
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
statusCode: 202,
|
|
139
|
+
body: { message: 'Trigger matched but no backend configured' },
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
const eventsHandler = async (req, parsed, iac) => {
|
|
143
|
+
return await servEvent(req, parsed, iac);
|
|
37
144
|
};
|
|
38
|
-
exports.
|
|
145
|
+
exports.eventsHandler = eventsHandler;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.functionsHandler = void 0;
|
|
7
|
+
const common_1 = require("../../common");
|
|
8
|
+
const functionRunner_1 = require("./functionRunner");
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const aliyunFc_1 = require("./aliyunFc");
|
|
12
|
+
const utils_1 = require("./utils");
|
|
13
|
+
const functionsHandler = async (req, parsed, iac) => {
|
|
14
|
+
common_1.logger.info(`Function request received by local server -> ${req.method} ${parsed.identifier ?? '/'} `);
|
|
15
|
+
const fcDef = iac.functions?.find((fn) => fn.key === parsed.identifier);
|
|
16
|
+
if (!fcDef) {
|
|
17
|
+
return {
|
|
18
|
+
statusCode: 404,
|
|
19
|
+
body: { error: 'Function not found', functionKey: parsed.identifier },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (!fcDef.code) {
|
|
23
|
+
return {
|
|
24
|
+
statusCode: 400,
|
|
25
|
+
body: { error: 'Function code configuration not found', functionKey: fcDef.key },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
let tempDir = null;
|
|
29
|
+
try {
|
|
30
|
+
const codePath = node_path_1.default.resolve(process.cwd(), fcDef.code.path);
|
|
31
|
+
let codeDir;
|
|
32
|
+
if (codePath.endsWith('.zip') && node_fs_1.default.existsSync(codePath)) {
|
|
33
|
+
tempDir = await (0, utils_1.extractZipFile)(codePath);
|
|
34
|
+
codeDir = tempDir;
|
|
35
|
+
}
|
|
36
|
+
else if (node_fs_1.default.existsSync(codePath) && node_fs_1.default.statSync(codePath).isDirectory()) {
|
|
37
|
+
codeDir = codePath;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
codeDir = node_path_1.default.dirname(codePath);
|
|
41
|
+
}
|
|
42
|
+
const funOptions = {
|
|
43
|
+
codeDir,
|
|
44
|
+
functionKey: fcDef.key,
|
|
45
|
+
handler: fcDef.code.handler,
|
|
46
|
+
servicePath: '',
|
|
47
|
+
timeout: fcDef.timeout * 1000,
|
|
48
|
+
};
|
|
49
|
+
// Check if provider is Aliyun to use Aliyun FC format
|
|
50
|
+
const isAliyun = iac.provider.name === common_1.ProviderEnum.ALIYUN;
|
|
51
|
+
let event;
|
|
52
|
+
let fcContext;
|
|
53
|
+
let env;
|
|
54
|
+
if (isAliyun) {
|
|
55
|
+
// Aliyun FC format: event is a Buffer containing JSON
|
|
56
|
+
const requestId = (0, aliyunFc_1.generateRequestId)();
|
|
57
|
+
const { event: aliyunEvent } = await (0, aliyunFc_1.transformToAliyunEvent)(req, parsed.url, parsed.query);
|
|
58
|
+
event = aliyunEvent;
|
|
59
|
+
// Use serializable context for worker thread (logger will be added inside worker)
|
|
60
|
+
fcContext = (0, aliyunFc_1.createAliyunContextSerializable)(iac, fcDef.name, fcDef.code.handler, fcDef.memory, fcDef.timeout, requestId);
|
|
61
|
+
env = {
|
|
62
|
+
...fcDef.environment,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// AWS Lambda format (default)
|
|
67
|
+
const rawBody = await (0, common_1.readRequestBody)(req);
|
|
68
|
+
event = rawBody ? JSON.parse(rawBody) : {};
|
|
69
|
+
env = {
|
|
70
|
+
...fcDef.environment,
|
|
71
|
+
AWS_REGION: iac.provider.region || 'us-east-1',
|
|
72
|
+
FUNCTION_NAME: fcDef.name,
|
|
73
|
+
FUNCTION_MEMORY_SIZE: String(fcDef.memory),
|
|
74
|
+
FUNCTION_TIMEOUT: String(fcDef.timeout),
|
|
75
|
+
};
|
|
76
|
+
fcContext = {
|
|
77
|
+
functionName: fcDef.name,
|
|
78
|
+
functionVersion: '$LATEST',
|
|
79
|
+
memoryLimitInMB: fcDef.memory,
|
|
80
|
+
logGroupName: `/aws/lambda/${fcDef.name}`,
|
|
81
|
+
logStreamName: `${new Date().toISOString().split('T')[0]}/[$LATEST]${Math.random().toString(36).substring(7)}`,
|
|
82
|
+
invokedFunctionArn: `arn:aws:lambda:${iac.provider.region}:000000000000:function:${fcDef.name}`,
|
|
83
|
+
awsRequestId: Math.random().toString(36).substring(2, 15),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
common_1.logger.debug(`Invoking worker with event type: ${isAliyun ? 'Buffer' : 'Object'} and context`);
|
|
87
|
+
common_1.logger.debug(`Worker codeDir: ${codeDir}, handler: ${funOptions.handler}`);
|
|
88
|
+
const result = await (0, functionRunner_1.invokeFunction)(funOptions, env, event, fcContext);
|
|
89
|
+
common_1.logger.info(`Function execution result: ${JSON.stringify(result)}`);
|
|
90
|
+
// For Aliyun, transform FC response to HTTP response if needed
|
|
91
|
+
if (isAliyun && result) {
|
|
92
|
+
const transformed = (0, aliyunFc_1.transformFCResponse)(result);
|
|
93
|
+
return {
|
|
94
|
+
statusCode: transformed.statusCode,
|
|
95
|
+
headers: transformed.headers,
|
|
96
|
+
body: transformed.body,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
statusCode: 200,
|
|
101
|
+
body: result,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
common_1.logger.error(`Function execution error: ${error}`);
|
|
106
|
+
return {
|
|
107
|
+
statusCode: 500,
|
|
108
|
+
body: {
|
|
109
|
+
error: 'Function execution failed',
|
|
110
|
+
message: error instanceof Error ? error.message : String(error),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
if (tempDir && node_fs_1.default.existsSync(tempDir)) {
|
|
116
|
+
node_fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
exports.functionsHandler = functionsHandler;
|