@gugananuvem/aws-local-simulator 1.0.11 → 1.0.12

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.
@@ -3,42 +3,40 @@
3
3
  */
4
4
 
5
5
  const HandlerLoader = require('./handler-loader');
6
- const RouteRegistry = require('./route-registry');
7
6
  const logger = require('../../utils/logger');
8
7
 
9
8
  class LambdaSimulator {
10
9
  constructor(config) {
11
10
  this.config = config;
12
- this.routeRegistry = new RouteRegistry();
13
- this.lambdas = new Map();
11
+ this.lambdas = new Map(); // functionName -> { handler, env, config }
14
12
  this.environment = { ...process.env };
15
13
  }
16
14
 
17
15
  async initialize() {
18
16
  logger.debug('Inicializando Lambda Simulator...');
19
-
17
+
20
18
  if (this.config.lambdas && this.config.lambdas.length > 0) {
21
19
  for (const lambdaConfig of this.config.lambdas) {
22
20
  await this.registerLambda(lambdaConfig);
23
21
  }
24
22
  }
25
-
23
+
26
24
  logger.debug(`✅ ${this.lambdas.size} Lambdas registradas`);
27
25
  }
28
26
 
29
27
  async registerLambda(lambdaConfig) {
30
28
  try {
31
- const { path, handler: handlerPath, env = {}, type = 'auto' } = lambdaConfig;
32
-
33
- // Carrega o handler
29
+ const { name, handler: handlerPath, env = {}, type = 'auto' } = lambdaConfig;
30
+
31
+ if (!name) {
32
+ logger.warn(`Lambda sem nome ignorada: ${JSON.stringify(lambdaConfig)}`);
33
+ return;
34
+ }
35
+
34
36
  const handler = await HandlerLoader.load(handlerPath, type);
35
-
36
- // Registra no route registry
37
- this.routeRegistry.register(path, handler, env);
38
-
39
- // Armazena metadata
40
- this.lambdas.set(path, {
41
- path,
37
+
38
+ this.lambdas.set(name, {
39
+ name,
42
40
  handler,
43
41
  handlerPath,
44
42
  handlerName: handler.name || 'anonymous',
@@ -46,84 +44,33 @@ class LambdaSimulator {
46
44
  type,
47
45
  registeredAt: new Date().toISOString()
48
46
  });
49
-
50
- logger.debug(`✅ Lambda registrada: ${path} -> ${handler.name || 'anonymous'}`);
51
-
47
+
48
+ logger.debug(`✅ Lambda registrada: ${name} -> ${handlerPath}`);
52
49
  } catch (error) {
53
- logger.error(`❌ Erro ao registrar Lambda ${lambdaConfig.path}:`, error);
50
+ logger.error(`❌ Erro ao registrar Lambda ${lambdaConfig.name}:`, error);
54
51
  throw error;
55
52
  }
56
53
  }
57
54
 
58
- async handleRequest(req, res) {
59
- const matchedRoute = this.routeRegistry.find(req.path);
60
-
61
- if (!matchedRoute) {
62
- return {
63
- error: {
64
- statusCode: 404,
65
- message: `Route not found: ${req.path}`,
66
- availableRoutes: this.listRoutes()
67
- },
68
- status: 404
69
- };
55
+ async invoke(functionName, event, invocationType = 'RequestResponse') {
56
+ const lambda = this.lambdas.get(functionName);
57
+
58
+ if (!lambda) {
59
+ throw new Error(`Function not found: ${functionName}`);
70
60
  }
71
-
72
- // Aplica variáveis de ambiente específicas da rota
73
- this.applyEnvironment(matchedRoute.env);
74
-
75
- // Prepara evento Lambda
76
- const event = this.toLambdaEvent(req, matchedRoute.params);
77
-
78
- logger.debug(`🎯 Executando: ${matchedRoute.path} -> ${matchedRoute.handler.name || 'anonymous'}`);
79
-
80
- // Executa middlewares
81
- const middlewares = this.routeRegistry.getMiddlewares(matchedRoute);
82
- let handled = false;
83
- let result = null;
84
-
85
- const runMiddlewares = async (index) => {
86
- if (index >= middlewares.length) {
87
- // Executa handler
88
61
 
89
- result = await this.executeHandler(matchedRoute.handler, event);
90
- handled = true;
91
-
92
- console.log(`✅ Resposta: ${result.statusCode}`);
93
- res
94
- .status(result.statusCode || 200)
95
- .set(result.headers || {})
96
- .send(result.body ? JSON.parse(result.body) : null);
97
- return;
98
- }
99
-
100
- const middleware = middlewares[index];
101
- await new Promise((resolve, reject) => {
102
- middleware(event, {
103
- status: (code) => ({ json: (data) => {
104
- result = { statusCode: code, body: data };
105
- handled = true;
106
- resolve();
107
- }}),
108
- send: (data) => {
109
- result = { statusCode: 200, body: data };
110
- handled = true;
111
- resolve();
112
- },
113
- next: () => {
114
- runMiddlewares(index + 1).then(resolve).catch(reject);
115
- }
116
- });
117
- });
118
- };
119
-
120
- await runMiddlewares(0);
121
-
122
- if (!handled && result) {
123
- return this.formatResponse(result);
62
+ this.applyEnvironment(lambda.env);
63
+ logger.debug(`🎯 Invocando Lambda: ${functionName}`);
64
+
65
+ if (invocationType === 'Event') {
66
+ this.executeHandler(lambda.handler, event).catch(err =>
67
+ logger.error(`❌ Async Lambda error (${functionName}):`, err)
68
+ );
69
+ return { StatusCode: 202 };
124
70
  }
125
71
 
126
- return null;
72
+ const result = await this.executeHandler(lambda.handler, event);
73
+ return { StatusCode: result.statusCode || 200, Payload: result };
127
74
  }
128
75
 
129
76
  async executeHandler(handler, event) {
@@ -135,57 +82,17 @@ class LambdaSimulator {
135
82
  logger.error('❌ Erro no handler:', error);
136
83
  return {
137
84
  statusCode: 500,
138
- body: {
139
- error: 'Internal Server Error',
140
- message: error.message,
141
- stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
142
- }
85
+ body: JSON.stringify({ error: 'Internal Server Error', message: error.message })
143
86
  };
144
87
  }
145
88
  }
146
89
 
147
- toLambdaEvent(req, params = {}) {
148
- return {
149
- httpMethod: req.method,
150
- path: req.path,
151
- headers: req.headers,
152
- queryStringParameters: req.query,
153
- pathParameters: params,
154
- body: req.body ? (typeof req.body === 'string' ? req.body : JSON.stringify(req.body)) : null,
155
- isBase64Encoded: false,
156
- requestContext: {
157
- path: req.path,
158
- stage: process.env.STAGE_NAME || 'dev',
159
- requestId: Math.random().toString(36).substring(7),
160
- identity: {
161
- sourceIp: req.ip,
162
- userAgent: req.headers['user-agent']
163
- }
164
- },
165
- stageVariables: {},
166
- resource: req.path
167
- };
168
- }
169
-
170
- formatResponse(result) {
171
- const statusCode = result.statusCode || 200;
172
- const body = result.body;
173
- const headers = result.headers || { 'Content-Type': 'application/json' };
174
-
175
- return {
176
- statusCode,
177
- headers,
178
- body: typeof body === 'string' ? body : JSON.stringify(body),
179
- isBase64Encoded: false
180
- };
181
- }
182
-
183
90
  createContext() {
184
91
  return {
185
92
  awsRequestId: Math.random().toString(36).substring(7),
186
93
  functionName: 'local-lambda',
187
94
  functionVersion: '$LATEST',
188
- invokedFunctionArn: 'arn:aws:lambda:local:function',
95
+ invokedFunctionArn: 'arn:aws:lambda:local:000000000000:function:local-lambda',
189
96
  memoryLimitInMB: '1024',
190
97
  logGroupName: '/aws/lambda/local-lambda',
191
98
  logStreamName: 'local-stream',
@@ -214,7 +121,7 @@ class LambdaSimulator {
214
121
 
215
122
  listLambdas() {
216
123
  return Array.from(this.lambdas.values()).map(l => ({
217
- path: l.path,
124
+ name: l.name,
218
125
  handlerName: l.handlerName,
219
126
  handlerPath: l.handlerPath,
220
127
  type: l.type,
@@ -223,12 +130,8 @@ class LambdaSimulator {
223
130
  }));
224
131
  }
225
132
 
226
- getLambda(path) {
227
- return this.lambdas.get(path);
228
- }
229
-
230
- listRoutes() {
231
- return this.routeRegistry.list();
133
+ getLambda(name) {
134
+ return this.lambdas.get(name);
232
135
  }
233
136
 
234
137
  getLambdasCount() {
@@ -237,49 +140,33 @@ class LambdaSimulator {
237
140
 
238
141
  async reloadLambdas() {
239
142
  logger.info('🔄 Recarregando Lambdas...');
240
-
241
- for (const [path, lambda] of this.lambdas.entries()) {
143
+
144
+ for (const [name, lambda] of this.lambdas.entries()) {
242
145
  try {
243
146
  const newHandler = await HandlerLoader.reload(lambda.handlerPath, lambda.type);
244
- this.routeRegistry.register(path, newHandler, lambda.env);
245
147
  lambda.handler = newHandler;
246
148
  lambda.handlerName = newHandler.name || 'anonymous';
247
- logger.debug(`✅ Lambda recarregada: ${path}`);
149
+ logger.debug(`✅ Lambda recarregada: ${name}`);
248
150
  } catch (error) {
249
- logger.error(`❌ Erro ao recarregar Lambda ${path}:`, error);
151
+ logger.error(`❌ Erro ao recarregar Lambda ${name}:`, error);
250
152
  }
251
153
  }
252
-
154
+
253
155
  logger.info(`✅ ${this.lambdas.size} Lambdas recarregadas`);
254
156
  }
255
157
 
256
158
  getStats() {
257
- const lambdas = this.listLambdas();
258
159
  return {
259
- totalLambdas: lambdas.length,
260
- lambdas: lambdas.map(l => ({
261
- path: l.path,
262
- handler: l.handlerName
263
- })),
264
- routes: this.routeRegistry.getStats(),
265
- environment: Object.keys(this.environment).length
160
+ totalLambdas: this.lambdas.size,
161
+ lambdas: this.listLambdas().map(l => ({ name: l.name, handler: l.handlerName }))
266
162
  };
267
163
  }
268
164
 
269
165
  async reset() {
270
- // Recarrega Lambdas
271
166
  await this.reloadLambdas();
272
-
273
- // Limpa variáveis de ambiente customizadas
274
- for (const key of Object.keys(this.environment)) {
275
- if (!process.env.hasOwnProperty(key) || key.startsWith('AWS_LOCAL_SIMULATOR_')) {
276
- delete process.env[key];
277
- }
278
- }
279
-
280
167
  this.environment = { ...process.env };
281
168
  logger.debug('Lambda: Estado resetado');
282
169
  }
283
170
  }
284
171
 
285
- module.exports = LambdaSimulator;
172
+ module.exports = LambdaSimulator;
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  const express = require('express');
6
+ const crypto = require('crypto');
6
7
  const SQSSimulator = require('./simulator');
7
8
  const logger = require('../../utils/logger');
8
9
 
@@ -18,8 +19,23 @@ class SQSServer {
18
19
  }
19
20
 
20
21
  setupMiddlewares() {
21
- this.app.use(express.json());
22
- this.app.use(express.urlencoded({ extended: true }));
22
+ this.app.use(express.raw({ type: '*/*', limit: '10mb' }));
23
+ this.app.use((req, res, next) => {
24
+ if (req.body && Buffer.isBuffer(req.body)) {
25
+ const str = req.body.toString('utf8');
26
+ const contentType = req.headers['content-type'] || '';
27
+ if (contentType.includes('application/x-amz-json-1.0') || contentType.includes('application/json')) {
28
+ try { req.body = JSON.parse(str); } catch (e) { req.body = {}; }
29
+ } else if (contentType.includes('application/x-www-form-urlencoded')) {
30
+ req.body = Object.fromEntries(new URLSearchParams(str));
31
+ } else {
32
+ try { req.body = JSON.parse(str); } catch (e) { req.body = {}; }
33
+ }
34
+ } else if (!req.body) {
35
+ req.body = {};
36
+ }
37
+ next();
38
+ });
23
39
 
24
40
  // Logging de requisições
25
41
  if (logger.currentLogLevel === 'verboso') {
@@ -42,16 +58,35 @@ class SQSServer {
42
58
 
43
59
  setupRoutes() {
44
60
  // Endpoint principal
61
+ this.app.use((req, res, next) => {
62
+ logger.info(`SQS incoming: ${req.method} ${req.path} headers=${JSON.stringify(req.headers)} query=${JSON.stringify(req.query)} body=${JSON.stringify(req.body)}`);
63
+ next();
64
+ });
65
+
45
66
  this.app.post('/', (req, res) => {
46
- const action = req.query.Action || req.body.Action;
67
+ const action = req.query.Action || req.body.Action ||
68
+ (req.headers['x-amz-target'] && req.headers['x-amz-target'].split('.')[1]);
69
+ logger.info(`SQS action resolved: ${action}`);
47
70
  const result = this.simulator.handleRequest(action, req, res);
48
-
71
+ const isJsonProtocol = req.headers['content-type'] && req.headers['content-type'].includes('application/x-amz-json-1.0');
72
+
49
73
  if (result && result.error) {
50
- res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
74
+ const isJsonProtocol = req.headers['content-type'] && req.headers['content-type'].includes('application/x-amz-json-1.0');
75
+ if (isJsonProtocol) {
76
+ res.status(result.status).json({ __type: result.error.code, message: result.error.message });
77
+ } else {
78
+ res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
79
+ }
51
80
  } else if (result) {
52
- // Gera resposta XML
53
- res.set('Content-Type', 'application/xml');
54
- res.send(this.generateResponse(action, result));
81
+ if (isJsonProtocol) {
82
+ res.set('Content-Type', 'application/x-amz-json-1.0');
83
+ res.json(this.generateJsonResponse(action, result));
84
+ } else {
85
+ const xml = this.generateResponse(action, result);
86
+ logger.info(`SQS response for ${action}: ${xml}`);
87
+ res.set('Content-Type', 'application/xml');
88
+ res.send(xml);
89
+ }
55
90
  }
56
91
  });
57
92
 
@@ -105,6 +140,37 @@ class SQSServer {
105
140
  });
106
141
  }
107
142
 
143
+ generateJsonResponse(action, result) {
144
+ switch (action) {
145
+ case 'CreateQueue':
146
+ return { QueueUrl: result.queueUrl };
147
+ case 'SendMessage':
148
+ return { MD5OfMessageBody: result.md5, MessageId: result.messageId };
149
+ case 'SendMessageBatch':
150
+ return {
151
+ Successful: (result.successful || []).map(s => ({ Id: s.Id, MessageId: s.MessageId, MD5OfMessageBody: s.MD5OfMessageBody })),
152
+ Failed: result.failed || []
153
+ };
154
+ case 'ReceiveMessage':
155
+ return {
156
+ Messages: (result.messages || []).map(m => ({
157
+ MessageId: m.MessageId,
158
+ ReceiptHandle: m.ReceiptHandle,
159
+ MD5OfBody: m.MD5OfBody,
160
+ Body: m.Body
161
+ }))
162
+ };
163
+ case 'DeleteMessage':
164
+ return {};
165
+ case 'GetQueueUrl':
166
+ return { QueueUrl: result.queueUrl };
167
+ case 'ListQueues':
168
+ return { QueueUrls: (result.queues || []).map(q => q.url) };
169
+ default:
170
+ return result;
171
+ }
172
+ }
173
+
108
174
  generateResponse(action, result) {
109
175
  switch(action) {
110
176
  case 'CreateQueue':
@@ -128,26 +194,28 @@ class SQSServer {
128
194
 
129
195
  generateCreateQueueResponse(queueUrl) {
130
196
  return `<?xml version="1.0" encoding="UTF-8"?>
131
- <CreateQueueResponse>
197
+ <CreateQueueResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/">
132
198
  <CreateQueueResult>
133
199
  <QueueUrl>${queueUrl}</QueueUrl>
134
200
  </CreateQueueResult>
201
+ <ResponseMetadata><RequestId>${crypto.randomUUID()}</RequestId></ResponseMetadata>
135
202
  </CreateQueueResponse>`;
136
203
  }
137
204
 
138
205
  generateSendMessageResponse(messageId, md5) {
139
206
  return `<?xml version="1.0" encoding="UTF-8"?>
140
- <SendMessageResponse>
207
+ <SendMessageResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/">
141
208
  <SendMessageResult>
142
209
  <MD5OfMessageBody>${md5}</MD5OfMessageBody>
143
210
  <MessageId>${messageId}</MessageId>
144
211
  </SendMessageResult>
212
+ <ResponseMetadata><RequestId>${crypto.randomUUID()}</RequestId></ResponseMetadata>
145
213
  </SendMessageResponse>`;
146
214
  }
147
215
 
148
216
  generateSendMessageBatchResponse(successful, failed) {
149
217
  let xml = `<?xml version="1.0" encoding="UTF-8"?>
150
- <SendMessageBatchResponse>
218
+ <SendMessageBatchResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/">
151
219
  <SendMessageBatchResult>`;
152
220
 
153
221
  for (const s of successful) {
@@ -170,6 +238,7 @@ class SQSServer {
170
238
 
171
239
  xml += `
172
240
  </SendMessageBatchResult>
241
+ <ResponseMetadata><RequestId>${crypto.randomUUID()}</RequestId></ResponseMetadata>
173
242
  </SendMessageBatchResponse>`;
174
243
 
175
244
  return xml;
@@ -177,7 +246,7 @@ class SQSServer {
177
246
 
178
247
  generateReceiveMessageResponse(messages) {
179
248
  let xml = `<?xml version="1.0" encoding="UTF-8"?>
180
- <ReceiveMessageResponse>
249
+ <ReceiveMessageResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/">
181
250
  <ReceiveMessageResult>`;
182
251
 
183
252
  for (const msg of messages) {
@@ -192,6 +261,7 @@ class SQSServer {
192
261
 
193
262
  xml += `
194
263
  </ReceiveMessageResult>
264
+ <ResponseMetadata><RequestId>${crypto.randomUUID()}</RequestId></ResponseMetadata>
195
265
  </ReceiveMessageResponse>`;
196
266
 
197
267
  return xml;
@@ -199,25 +269,26 @@ class SQSServer {
199
269
 
200
270
  generateDeleteMessageResponse() {
201
271
  return `<?xml version="1.0" encoding="UTF-8"?>
202
- <DeleteMessageResponse>
272
+ <DeleteMessageResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/">
203
273
  <ResponseMetadata>
204
- <RequestId>${Math.random().toString(36).substring(7)}</RequestId>
274
+ <RequestId>${crypto.randomUUID()}</RequestId>
205
275
  </ResponseMetadata>
206
276
  </DeleteMessageResponse>`;
207
277
  }
208
278
 
209
279
  generateGetQueueUrlResponse(queueUrl) {
210
280
  return `<?xml version="1.0" encoding="UTF-8"?>
211
- <GetQueueUrlResponse>
281
+ <GetQueueUrlResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/">
212
282
  <GetQueueUrlResult>
213
283
  <QueueUrl>${queueUrl}</QueueUrl>
214
284
  </GetQueueUrlResult>
285
+ <ResponseMetadata><RequestId>${crypto.randomUUID()}</RequestId></ResponseMetadata>
215
286
  </GetQueueUrlResponse>`;
216
287
  }
217
288
 
218
289
  generateListQueuesResponse(queues) {
219
290
  let xml = `<?xml version="1.0" encoding="UTF-8"?>
220
- <ListQueuesResponse>
291
+ <ListQueuesResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/">
221
292
  <ListQueuesResult>`;
222
293
 
223
294
  for (const queue of queues) {
@@ -226,6 +297,7 @@ class SQSServer {
226
297
 
227
298
  xml += `
228
299
  </ListQueuesResult>
300
+ <ResponseMetadata><RequestId>${crypto.randomUUID()}</RequestId></ResponseMetadata>
229
301
  </ListQueuesResponse>`;
230
302
 
231
303
  return xml;