@gugananuvem/aws-local-simulator 1.0.29 → 1.0.31

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 CHANGED
@@ -33,7 +33,7 @@ Simulador local completo para serviços AWS. Desenvolva e teste suas aplicaçõe
33
33
  ## 📦 Instalação
34
34
 
35
35
  ```bash
36
- npm install --save-dev aws-local-simulator
36
+ npm install --save-dev @gugananuvem/aws-local-simulator
37
37
  ```
38
38
 
39
39
  ## 🚀 Uso Rápido
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gugananuvem/aws-local-simulator",
3
- "version": "1.0.29",
3
+ "version": "1.0.31",
4
4
  "description": "Simulador local completo para serviços AWS",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -66,6 +66,6 @@
66
66
  "publishConfig": {
67
67
  "directory": "dist"
68
68
  },
69
- "buildDate": "2026-04-30T19:05:54.245Z",
69
+ "buildDate": "2026-05-05T10:06:53.890Z",
70
70
  "published": true
71
71
  }
@@ -164,6 +164,8 @@ class CognitoServer {
164
164
  return this.simulator.adminListGroupsForUser(params);
165
165
  case 'AdminUserGlobalSignOut':
166
166
  return this.simulator.adminUserGlobalSignOut(params);
167
+ case 'AdminUpdateUserAttributes':
168
+ return this.simulator.adminUpdateUserAttributes(params);
167
169
 
168
170
  // Identity Pool Operations
169
171
  case 'CreateIdentityPool':
@@ -1222,6 +1222,23 @@ class CognitoSimulator {
1222
1222
  throw new Error(`User pool ${UserPoolId} not found`);
1223
1223
  }
1224
1224
 
1225
+ // Check if a user with the same Username or email already exists in this pool
1226
+ const normalizedAttrs = this.normalizeUserAttributes(UserAttributes || []);
1227
+ const emailToCheck = normalizedAttrs.email;
1228
+
1229
+ const existingUser = Array.from(this.users.values()).find((u) => {
1230
+ if (u.UserPoolId !== UserPoolId) return false;
1231
+ if (u.Username === Username) return true;
1232
+ if (emailToCheck && u.Attributes?.email === emailToCheck) return true;
1233
+ return false;
1234
+ });
1235
+
1236
+ if (existingUser) {
1237
+ const err = new Error(`User account already exists`);
1238
+ err.code = "UsernameExistsException";
1239
+ throw err;
1240
+ }
1241
+
1225
1242
  const tempPassword = TemporaryPassword || this._generateTemporaryPassword();
1226
1243
 
1227
1244
  const userId = uuidv4();
@@ -1229,7 +1246,7 @@ class CognitoSimulator {
1229
1246
  Username: Username,
1230
1247
  UserPoolId: UserPoolId,
1231
1248
  UserId: userId,
1232
- Attributes: this.normalizeUserAttributes(UserAttributes || []),
1249
+ Attributes: normalizedAttrs,
1233
1250
  Enabled: true,
1234
1251
  UserStatus: "FORCE_CHANGE_PASSWORD",
1235
1252
  CreatedDate: new Date().toISOString(),
@@ -1242,7 +1259,7 @@ class CognitoSimulator {
1242
1259
 
1243
1260
  // PreSignUp trigger — dispara antes de criar o usuário (admin context)
1244
1261
  const preSignUpEvent = this._buildTriggerEvent("PreSignUp_AdminCreateUser", userPool, user, "ADMIN", {
1245
- userAttributes: this.normalizeUserAttributes(UserAttributes || []),
1262
+ userAttributes: normalizedAttrs,
1246
1263
  validationData: {},
1247
1264
  clientMetadata: {},
1248
1265
  });
@@ -1284,6 +1301,28 @@ class CognitoSimulator {
1284
1301
  return password.sort(() => Math.random() - 0.5).join("");
1285
1302
  }
1286
1303
 
1304
+ adminUpdateUserAttributes(params) {
1305
+ const { UserPoolId, Username, UserAttributes } = params;
1306
+ const userPool = this.userPools.get(UserPoolId);
1307
+
1308
+ if (!userPool) {
1309
+ throw new Error(`User pool ${UserPoolId} not found`);
1310
+ }
1311
+
1312
+ const user = this.findUserByUsername(Username, null, UserPoolId);
1313
+ if (!user) {
1314
+ throw new Error(`User not found: ${Username}`);
1315
+ }
1316
+
1317
+ const updates = this.normalizeUserAttributes(UserAttributes || []);
1318
+ Object.assign(user.Attributes, updates);
1319
+ user.LastModifiedDate = Math.floor(Date.now() / 1000);
1320
+ this.persistUsers();
1321
+
1322
+ logger.debug(`✅ AdminUpdateUserAttributes: ${Username} in ${UserPoolId}`);
1323
+ return {};
1324
+ }
1325
+
1287
1326
  adminSetUserPassword(params) {
1288
1327
  const { UserPoolId, Username, Password, Permanent } = params;
1289
1328
  const user = this.findUserByUsername(Username, null, UserPoolId);
@@ -85,7 +85,16 @@ class DynamoDBServer {
85
85
  });
86
86
 
87
87
  this.app.get('/__admin/tables/:tableName/items', (req, res) => {
88
- const items = this.simulator.scan({ TableName: req.params.tableName });
88
+ const params = { TableName: req.params.tableName };
89
+ if (req.query.Limit) params.Limit = Number(req.query.Limit);
90
+ if (req.query.ExclusiveStartKey) {
91
+ try {
92
+ params.ExclusiveStartKey = JSON.parse(req.query.ExclusiveStartKey);
93
+ } catch (e) {
94
+ // Ignore invalid JSON for ExclusiveStartKey
95
+ }
96
+ }
97
+ const items = this.simulator.scan(params);
89
98
  res.json(items);
90
99
  });
91
100
 
@@ -550,7 +550,18 @@ class DynamoDBSimulator {
550
550
  }
551
551
 
552
552
  query(params) {
553
- const { TableName, KeyConditionExpression, ExpressionAttributeValues, ExpressionAttributeNames = {}, IndexName } = params;
553
+ const {
554
+ TableName,
555
+ KeyConditionExpression,
556
+ FilterExpression,
557
+ ExpressionAttributeValues,
558
+ ExpressionAttributeNames = {},
559
+ IndexName,
560
+ Limit,
561
+ ExclusiveStartKey,
562
+ ProjectionExpression,
563
+ ScanIndexForward = true
564
+ } = params;
554
565
  const table = this.tables.get(TableName);
555
566
 
556
567
  if (!table) {
@@ -559,21 +570,13 @@ class DynamoDBSimulator {
559
570
 
560
571
  let items = this.store.read(TableName);
561
572
 
562
- // Resolve hash key e range key
563
- let hashKey;
564
- let rangeKey;
565
-
566
- if (IndexName != null) {
567
- const gsiDefs = table.globalSecondaryIndexes || {};
568
- const gsi = gsiDefs[IndexName];
569
- if (!gsi) {
570
- throw new Error(`GSI "${IndexName}" not found on table "${TableName}"`);
573
+ // Se for consulta por índice, filtra itens que não possuem as chaves do índice (Sparse Index)
574
+ if (IndexName && table.globalSecondaryIndexes?.[IndexName]) {
575
+ const gsi = table.globalSecondaryIndexes[IndexName];
576
+ items = items.filter(item => item[gsi.hashKey] !== undefined);
577
+ if (gsi.rangeKey) {
578
+ items = items.filter(item => item[gsi.rangeKey] !== undefined);
571
579
  }
572
- hashKey = gsi.hashKey;
573
- rangeKey = gsi.rangeKey;
574
- } else {
575
- hashKey = table.hashKey;
576
- rangeKey = table.rangeKey;
577
580
  }
578
581
 
579
582
  // Helper para resolver nomes de atributos (que podem ser placeholders como #n0)
@@ -586,7 +589,6 @@ class DynamoDBSimulator {
586
589
  const resolveValue = (placeholder) => {
587
590
  const rawValue = ExpressionAttributeValues[placeholder];
588
591
  if (rawValue === undefined) return undefined;
589
- // Se for formato DynamoDB { S: "..." }, desmembra. Se for nativo (DocumentClient), usa direto.
590
592
  if (rawValue !== null && typeof rawValue === 'object' && !Array.isArray(rawValue)) {
591
593
  const keys = Object.keys(rawValue);
592
594
  if (keys.length === 1 && ["S", "N", "BOOL", "NULL", "M", "L", "SS", "NS", "BS"].includes(keys[0])) {
@@ -597,31 +599,23 @@ class DynamoDBSimulator {
597
599
  };
598
600
 
599
601
  // Filtra pela KeyConditionExpression
600
- // DynamoDB Query KeyConditionExpression tem formato restrito: PartitionKey = :val AND (SortKey operator :val)
601
602
  if (KeyConditionExpression) {
602
603
  const parts = KeyConditionExpression.split(/\s+AND\s+/i);
603
-
604
604
  for (const part of parts) {
605
605
  const match = part.match(/([^\s]+)\s*(=|>|<|>=|<=|BEGINS_WITH|BETWEEN)\s*([^\s]+)(?:\s+AND\s+([^\s]+))?/i);
606
606
  if (match) {
607
607
  const attrPlaceholder = match[1];
608
608
  const operator = match[2].toUpperCase();
609
609
  const valPlaceholder = match[3];
610
-
611
610
  const attributeName = resolveAttributeName(attrPlaceholder);
612
611
  const expectedValue = resolveValue(valPlaceholder);
613
612
 
614
- if (operator === "=") {
615
- items = items.filter(item => item[attributeName] === expectedValue);
616
- } else if (operator === ">") {
617
- items = items.filter(item => item[attributeName] > expectedValue);
618
- } else if (operator === "<") {
619
- items = items.filter(item => item[attributeName] < expectedValue);
620
- } else if (operator === ">=") {
621
- items = items.filter(item => item[attributeName] >= expectedValue);
622
- } else if (operator === "<=") {
623
- items = items.filter(item => item[attributeName] <= expectedValue);
624
- } else if (operator === "BEGINS_WITH") {
613
+ if (operator === "=") items = items.filter(item => item[attributeName] === expectedValue);
614
+ else if (operator === ">") items = items.filter(item => item[attributeName] > expectedValue);
615
+ else if (operator === "<") items = items.filter(item => item[attributeName] < expectedValue);
616
+ else if (operator === ">=") items = items.filter(item => item[attributeName] >= expectedValue);
617
+ else if (operator === "<=") items = items.filter(item => item[attributeName] <= expectedValue);
618
+ else if (operator === "BEGINS_WITH") {
625
619
  const val = expectedValue;
626
620
  items = items.filter(item => String(item[attributeName] || "").startsWith(String(val)));
627
621
  }
@@ -629,44 +623,148 @@ class DynamoDBSimulator {
629
623
  }
630
624
  }
631
625
 
632
- const marshalledItems = items.map((item) => this.marshallItem(item, table));
626
+ // Ordenação (DynamoDB sempre ordena pela Sort Key)
627
+ let sortKey = table.rangeKey;
628
+ if (IndexName && table.globalSecondaryIndexes?.[IndexName]) {
629
+ sortKey = table.globalSecondaryIndexes[IndexName].rangeKey;
630
+ }
631
+
632
+ if (sortKey) {
633
+ items.sort((a, b) => {
634
+ const valA = a[sortKey];
635
+ const valB = b[sortKey];
636
+
637
+ if (valA === valB) return 0;
638
+ if (valA === undefined || valA === null) return 1;
639
+ if (valB === undefined || valB === null) return -1;
640
+
641
+ let comparison = 0;
642
+ if (typeof valA === 'number' && typeof valB === 'number') {
643
+ comparison = valA - valB;
644
+ } else {
645
+ comparison = String(valA).localeCompare(String(valB));
646
+ }
647
+
648
+ return ScanIndexForward ? comparison : -comparison;
649
+ });
650
+ }
651
+
652
+ const scannedCount = items.length;
653
+
654
+ // Aplica FilterExpression se existir
655
+ if (FilterExpression) {
656
+ items = this.applyFilter(items, FilterExpression, ExpressionAttributeValues, ExpressionAttributeNames, table);
657
+ }
658
+
659
+ const totalMatchingCount = items.length;
660
+
661
+ // Apply Pagination (ExclusiveStartKey)
662
+ if (ExclusiveStartKey) {
663
+ const startKeyStr = this.getItemKeyFromKeys(ExclusiveStartKey, table);
664
+ const startIndex = items.findIndex(item => this.getItemKey(item, table) === startKeyStr);
665
+ if (startIndex !== -1) {
666
+ items = items.slice(startIndex + 1);
667
+ }
668
+ }
669
+
670
+ // Apply Limit
671
+ let lastEvaluatedKey = null;
672
+ if (Limit && items.length > Limit) {
673
+ const lastItem = items[Limit - 1];
674
+ lastEvaluatedKey = this.marshallItem(lastItem, table);
675
+ items = items.slice(0, Limit);
676
+ }
677
+
678
+ let marshalledItems = items.map((item) => this.marshallItem(item, table));
679
+
680
+ // Apply Projection
681
+ if (ProjectionExpression) {
682
+ marshalledItems = this.applyProjection(marshalledItems, ProjectionExpression, ExpressionAttributeNames);
683
+ }
633
684
 
634
685
  return {
635
686
  Items: marshalledItems,
636
687
  Count: marshalledItems.length,
637
- ScannedCount: items.length,
688
+ ScannedCount: scannedCount,
689
+ LastEvaluatedKey: lastEvaluatedKey || undefined
638
690
  };
639
691
  }
640
692
 
641
693
  scan(params) {
642
- const { TableName, FilterExpression, ExpressionAttributeValues, ExpressionAttributeNames = {}, Limit } = params;
694
+ const {
695
+ TableName,
696
+ FilterExpression,
697
+ ExpressionAttributeValues,
698
+ ExpressionAttributeNames = {},
699
+ Limit,
700
+ ExclusiveStartKey,
701
+ ProjectionExpression
702
+ } = params;
643
703
  const table = this.tables.get(TableName);
644
704
 
645
705
  if (!table) {
646
706
  throw new Error(`Table ${TableName} does not exist`);
647
707
  }
648
708
 
649
- let items = this.store.read(TableName);
709
+ const allItems = this.store.read(TableName);
710
+ let items = allItems;
711
+ const scannedCount = items.length;
650
712
 
651
713
  // Aplica filtro se existir
652
714
  if (FilterExpression) {
653
715
  items = this.applyFilter(items, FilterExpression, ExpressionAttributeValues, ExpressionAttributeNames, table);
654
716
  }
655
717
 
656
- // Aplica limite
718
+ // Apply Pagination (ExclusiveStartKey)
719
+ if (ExclusiveStartKey) {
720
+ const startKeyStr = this.getItemKeyFromKeys(ExclusiveStartKey, table);
721
+ const startIndex = items.findIndex(item => this.getItemKey(item, table) === startKeyStr);
722
+ if (startIndex !== -1) {
723
+ items = items.slice(startIndex + 1);
724
+ }
725
+ }
726
+
727
+ // Apply Limit
728
+ let lastEvaluatedKey = null;
657
729
  if (Limit && items.length > Limit) {
730
+ const lastItem = items[Limit - 1];
731
+ lastEvaluatedKey = this.marshallItem(lastItem, table);
658
732
  items = items.slice(0, Limit);
659
733
  }
660
734
 
661
- const marshalledItems = items.map((item) => this.marshallItem(item, table));
735
+ let marshalledItems = items.map((item) => this.marshallItem(item, table));
736
+
737
+ // Apply Projection
738
+ if (ProjectionExpression) {
739
+ marshalledItems = this.applyProjection(marshalledItems, ProjectionExpression, ExpressionAttributeNames);
740
+ }
662
741
 
663
742
  return {
664
743
  Items: marshalledItems,
665
744
  Count: marshalledItems.length,
666
- ScannedCount: items.length,
745
+ ScannedCount: scannedCount,
746
+ LastEvaluatedKey: lastEvaluatedKey || undefined
667
747
  };
668
748
  }
669
749
 
750
+ applyProjection(items, expression, names = {}) {
751
+ const projectedAttrs = expression.split(',').map(s => s.trim()).filter(Boolean);
752
+ const resolvedAttrs = projectedAttrs.map(attr => {
753
+ if (attr.startsWith("#")) return names[attr] || attr;
754
+ return attr;
755
+ });
756
+
757
+ return items.map(item => {
758
+ const newItem = {};
759
+ resolvedAttrs.forEach(attr => {
760
+ if (item[attr] !== undefined) {
761
+ newItem[attr] = item[attr];
762
+ }
763
+ });
764
+ return newItem;
765
+ });
766
+ }
767
+
670
768
 
671
769
  // Métodos auxiliares
672
770
  normalizeItem(item, table) {
@@ -748,8 +846,8 @@ class DynamoDBSimulator {
748
846
  const [path, valueExpr] = assignment.split("=").map((s) => s.trim());
749
847
  const attributeName = nameMap[path] || path.replace(/#/g, "");
750
848
  const rawValue = valueMap[valueExpr];
751
- const value = rawValue && typeof rawValue === 'object' ? Object.values(rawValue)[0] : rawValue;
752
- item[attributeName] = value;
849
+ // Usa normalizeValue para garantir o mesmo formato que o putItem
850
+ item[attributeName] = this.normalizeValue(rawValue, table);
753
851
  }
754
852
  }
755
853
 
@@ -761,7 +859,8 @@ class DynamoDBSimulator {
761
859
  const parts = assignment.split(/\s+/);
762
860
  const attributeName = nameMap[parts[0]] || parts[0].replace(/#/g, "");
763
861
  const rawValue = valueMap[parts[1]];
764
- const delta = rawValue && typeof rawValue === 'object' ? Object.values(rawValue)[0] : rawValue;
862
+ // Usa normalizeValue para garantir o mesmo formato que o putItem
863
+ const delta = this.normalizeValue(rawValue, table);
765
864
  const current = item[attributeName];
766
865
  if (current === undefined || current === null) {
767
866
  item[attributeName] = typeof delta === 'number' ? delta : parseFloat(delta) || 0;
@@ -36,6 +36,9 @@ class LambdaService {
36
36
  injectDependencies(server) {
37
37
  const ct = server.getService('cloudtrail');
38
38
  if (ct?.simulator) this.simulator.audit.setTrail(ct.simulator);
39
+
40
+ const cw = server.getService('cloudwatch');
41
+ if (cw?.simulator) this.simulator.cloudwatchSimulator = cw.simulator;
39
42
  }
40
43
 
41
44
  async start() {
@@ -18,7 +18,6 @@ class LambdaServer {
18
18
  }
19
19
 
20
20
  setupMiddlewares() {
21
- this.app.use(express.json({ limit: '10mb' }));
22
21
  this.app.use(express.urlencoded({ extended: true }));
23
22
  this.app.use(cors());
24
23
 
@@ -51,7 +50,22 @@ class LambdaServer {
51
50
  this.app.post('/2015-03-31/functions/:functionName/invocations', async (req, res) => {
52
51
  const { functionName } = req.params;
53
52
  const invocationType = req.headers['x-amz-invocation-type'] || 'RequestResponse';
54
- const event = req.body || {};
53
+
54
+ // Read body directly from stream, bypassing all body parsers
55
+ let event = {};
56
+ try {
57
+ const rawBody = await new Promise((resolve, reject) => {
58
+ const chunks = [];
59
+ req.on('data', chunk => chunks.push(chunk));
60
+ req.on('end', () => resolve(Buffer.concat(chunks)));
61
+ req.on('error', reject);
62
+ });
63
+ if (rawBody.length > 0) {
64
+ event = JSON.parse(rawBody.toString('utf8'));
65
+ }
66
+ } catch {
67
+ event = {};
68
+ }
55
69
 
56
70
  logger.debug(`Lambda invoke: ${functionName} (${invocationType})`);
57
71
 
@@ -77,11 +91,19 @@ class LambdaServer {
77
91
  }
78
92
 
79
93
  setupAdminRoutes() {
94
+ // Helper: parse Buffer body as JSON for admin routes
95
+ const parseJson = (req, res, next) => {
96
+ if (Buffer.isBuffer(req.body) && req.body.length > 0) {
97
+ try { req.body = JSON.parse(req.body.toString('utf8')); } catch { req.body = {}; }
98
+ }
99
+ next();
100
+ };
101
+
80
102
  this.app.get('/__admin/functions', (req, res) => {
81
103
  res.json(this.simulator.listLambdas());
82
104
  });
83
105
 
84
- this.app.post('/__admin/functions', async (req, res) => {
106
+ this.app.post('/__admin/functions', parseJson, async (req, res) => {
85
107
  try {
86
108
  const lambda = await this.simulator.createFunction(req.body);
87
109
  res.status(201).json(lambda);
@@ -90,7 +112,7 @@ class LambdaServer {
90
112
  }
91
113
  });
92
114
 
93
- this.app.put('/__admin/functions/:name', async (req, res) => {
115
+ this.app.put('/__admin/functions/:name', parseJson, async (req, res) => {
94
116
  try {
95
117
  const lambda = await this.simulator.updateFunction(req.params.name, req.body);
96
118
  res.json(lambda);
@@ -110,7 +132,7 @@ class LambdaServer {
110
132
  res.json({ message: 'Lambdas recarregadas', count: this.simulator.getLambdasCount() });
111
133
  });
112
134
 
113
- this.app.post('/__admin/env', (req, res) => {
135
+ this.app.post('/__admin/env', parseJson, (req, res) => {
114
136
  const { key, value } = req.body;
115
137
  if (key && value !== undefined) {
116
138
  this.simulator.setEnvironmentVariable(key, value);
@@ -13,6 +13,7 @@ class LambdaSimulator {
13
13
  this.lambdas = new Map(); // functionName -> { handler, env, config }
14
14
  this.environment = { ...process.env };
15
15
  this.audit = new CloudTrailAudit("lambda.amazonaws.com");
16
+ this.cloudwatchSimulator = null; // injected via injectDependencies
16
17
  }
17
18
 
18
19
  async initialize() {
@@ -108,13 +109,13 @@ class LambdaSimulator {
108
109
  logger.debug(`🎯 Invocando Lambda: ${functionName}`);
109
110
 
110
111
  if (invocationType === "Event") {
111
- this.executeHandler(lambda.handler, event).catch((err) => logger.error(`❌ Async Lambda error (${functionName}):`, err));
112
+ this.executeHandler(lambda, functionName, event).catch((err) => logger.error(`❌ Async Lambda error (${functionName}):`, err));
112
113
  return { StatusCode: 202 };
113
114
  }
114
115
 
115
116
  let result;
116
117
  try {
117
- result = await this.executeHandler(lambda.handler, event);
118
+ result = await this.executeHandler(lambda, functionName, event);
118
119
  } catch (error) {
119
120
  logger.error(`❌ Lambda handler error (${functionName}):`, error);
120
121
  throw error;
@@ -128,21 +129,63 @@ class LambdaSimulator {
128
129
  return { StatusCode: result.statusCode || 200, Payload: result };
129
130
  }
130
131
 
131
- async executeHandler(handler, event) {
132
- const context = this.createContext();
133
- const result = await handler(event, context);
132
+ async executeHandler(lambda, functionName, event) {
133
+ const requestId = Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10);
134
+ const capturedLogs = [];
135
+
136
+ const context = this.createContext(functionName, requestId);
137
+
138
+ // Intercept console output during handler execution
139
+ const origLog = console.log;
140
+ const origError = console.error;
141
+ const origWarn = console.warn;
142
+ const origInfo = console.info;
143
+
144
+ const capture = (...args) => {
145
+ const line = args.map(a => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ');
146
+ capturedLogs.push(line);
147
+ };
148
+
149
+ console.log = (...args) => { capture(...args); origLog(...args); };
150
+ console.error = (...args) => { capture(`[ERROR] ${args.map(a => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ')}`); origError(...args); };
151
+ console.warn = (...args) => { capture(`[WARN] ${args.map(a => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ')}`); origWarn(...args); };
152
+ console.info = (...args) => { capture(`[INFO] ${args.map(a => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ')}`); origInfo(...args); };
153
+
154
+ let result;
155
+ let execError;
156
+ try {
157
+ result = await lambda.handler(event, context);
158
+ } catch (err) {
159
+ execError = err;
160
+ capturedLogs.push(`[ERROR] ${err.message}`);
161
+ } finally {
162
+ console.log = origLog;
163
+ console.error = origError;
164
+ console.warn = origWarn;
165
+ console.info = origInfo;
166
+ }
167
+
168
+ // Send logs to CloudWatch asynchronously (non-blocking)
169
+ if (this.cloudwatchSimulator) {
170
+ this.cloudwatchSimulator
171
+ .putLambdaLogs(functionName, requestId, capturedLogs)
172
+ .catch((err) => logger.debug(`[CloudWatch] Failed to store Lambda logs: ${err.message}`));
173
+ }
174
+
175
+ if (execError) throw execError;
134
176
  return result;
135
177
  }
136
178
 
137
- createContext() {
179
+ createContext(functionName = "local-lambda", requestId = null) {
180
+ const reqId = requestId || Math.random().toString(36).substring(2, 18);
138
181
  return {
139
- awsRequestId: Math.random().toString(36).substring(7),
140
- functionName: "local-lambda",
182
+ awsRequestId: reqId,
183
+ functionName,
141
184
  functionVersion: "$LATEST",
142
- invokedFunctionArn: "arn:aws:lambda:local:000000000000:function:local-lambda",
185
+ invokedFunctionArn: `arn:aws:lambda:local:000000000000:function:${functionName}`,
143
186
  memoryLimitInMB: "1024",
144
- logGroupName: "/aws/lambda/local-lambda",
145
- logStreamName: "local-stream",
187
+ logGroupName: `/aws/lambda/${functionName}`,
188
+ logStreamName: `${new Date().toISOString().slice(0, 10).replace(/-/g, '/')}/${reqId.slice(0, 8)}`,
146
189
  getRemainingTimeInMillis: () => 30000,
147
190
  callbackWaitsForEmptyEventLoop: true,
148
191
  identity: null,