@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 +1 -1
- package/package.json +2 -2
- package/src/services/cognito/server.js +2 -0
- package/src/services/cognito/simulator.js +41 -2
- package/src/services/dynamodb/server.js +10 -1
- package/src/services/dynamodb/simulator.js +139 -40
- package/src/services/lambda/index.js +3 -0
- package/src/services/lambda/server.js +27 -5
- package/src/services/lambda/simulator.js +54 -11
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gugananuvem/aws-local-simulator",
|
|
3
|
-
"version": "1.0.
|
|
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-
|
|
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:
|
|
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:
|
|
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
|
|
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 {
|
|
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
|
-
//
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
|
|
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:
|
|
688
|
+
ScannedCount: scannedCount,
|
|
689
|
+
LastEvaluatedKey: lastEvaluatedKey || undefined
|
|
638
690
|
};
|
|
639
691
|
}
|
|
640
692
|
|
|
641
693
|
scan(params) {
|
|
642
|
-
const {
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
752
|
-
item[attributeName] =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
132
|
-
const
|
|
133
|
-
const
|
|
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:
|
|
140
|
-
functionName
|
|
182
|
+
awsRequestId: reqId,
|
|
183
|
+
functionName,
|
|
141
184
|
functionVersion: "$LATEST",
|
|
142
|
-
invokedFunctionArn:
|
|
185
|
+
invokedFunctionArn: `arn:aws:lambda:local:000000000000:function:${functionName}`,
|
|
143
186
|
memoryLimitInMB: "1024",
|
|
144
|
-
logGroupName:
|
|
145
|
-
logStreamName:
|
|
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,
|