@gugananuvem/aws-local-simulator 1.0.30 → 1.0.33
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/simulator.js +28 -11
- package/src/services/dynamodb/server.js +10 -1
- package/src/services/dynamodb/simulator.js +152 -41
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.33",
|
|
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-05-
|
|
69
|
+
"buildDate": "2026-05-07T01:08:47.293Z",
|
|
70
70
|
"published": true
|
|
71
71
|
}
|
|
@@ -268,8 +268,8 @@ class CognitoSimulator {
|
|
|
268
268
|
Username: u.Username,
|
|
269
269
|
UserStatus: u.UserStatus,
|
|
270
270
|
Enabled: u.Enabled,
|
|
271
|
-
UserCreateDate: u.CreatedDate,
|
|
272
|
-
UserLastModifiedDate: u.LastModifiedDate,
|
|
271
|
+
UserCreateDate: new Date(u.CreatedDate).getTime() / 1000,
|
|
272
|
+
UserLastModifiedDate: new Date(u.LastModifiedDate).getTime() / 1000,
|
|
273
273
|
Attributes: this._formatUserAttributesWithSub(u),
|
|
274
274
|
})),
|
|
275
275
|
PaginationToken: nextToken,
|
|
@@ -1204,8 +1204,8 @@ class CognitoSimulator {
|
|
|
1204
1204
|
return {
|
|
1205
1205
|
Username: user.Username,
|
|
1206
1206
|
UserAttributes: this._formatUserAttributesWithSub(user),
|
|
1207
|
-
UserCreateDate: user.CreatedDate,
|
|
1208
|
-
UserLastModifiedDate: user.LastModifiedDate,
|
|
1207
|
+
UserCreateDate: new Date(user.CreatedDate).getTime() / 1000,
|
|
1208
|
+
UserLastModifiedDate: new Date(user.LastModifiedDate).getTime() / 1000,
|
|
1209
1209
|
Enabled: user.Enabled,
|
|
1210
1210
|
UserStatus: user.UserStatus,
|
|
1211
1211
|
MFAOptions: user.MfaOptions,
|
|
@@ -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
|
});
|
|
@@ -1262,12 +1279,12 @@ class CognitoSimulator {
|
|
|
1262
1279
|
return {
|
|
1263
1280
|
User: {
|
|
1264
1281
|
Username: user.Username,
|
|
1265
|
-
|
|
1266
|
-
UserCreateDate: user.CreatedDate,
|
|
1267
|
-
UserLastModifiedDate: user.LastModifiedDate,
|
|
1282
|
+
Attributes: this._formatUserAttributesWithSub(user),
|
|
1283
|
+
UserCreateDate: new Date(user.CreatedDate).getTime() / 1000,
|
|
1284
|
+
UserLastModifiedDate: new Date(user.LastModifiedDate).getTime() / 1000,
|
|
1268
1285
|
Enabled: user.Enabled,
|
|
1269
1286
|
UserStatus: user.UserStatus,
|
|
1270
|
-
|
|
1287
|
+
MFAOptions: user.MfaOptions || [],
|
|
1271
1288
|
},
|
|
1272
1289
|
};
|
|
1273
1290
|
}
|
|
@@ -1489,7 +1506,7 @@ class CognitoSimulator {
|
|
|
1489
1506
|
AccessKeyId: `AKIA${crypto.randomBytes(16).toString("hex").toUpperCase()}`,
|
|
1490
1507
|
SecretKey: crypto.randomBytes(32).toString("hex"),
|
|
1491
1508
|
SessionToken: crypto.randomBytes(64).toString("base64"),
|
|
1492
|
-
Expiration: new Date(Date.now() + 3600000)
|
|
1509
|
+
Expiration: new Date(Date.now() + 3600000),
|
|
1493
1510
|
};
|
|
1494
1511
|
|
|
1495
1512
|
return {
|
|
@@ -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,35 @@ 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
|
-
const
|
|
605
|
+
const trimmedPart = part.trim();
|
|
606
|
+
|
|
607
|
+
// Tenta match de função: begins_with(attr, :val)
|
|
608
|
+
const funcMatch = trimmedPart.match(/^begins_with\s*\(\s*([^\s,]+)\s*,\s*([^\s,)]+)\s*\)$/i);
|
|
609
|
+
if (funcMatch) {
|
|
610
|
+
const attributeName = resolveAttributeName(funcMatch[1]);
|
|
611
|
+
const expectedValue = resolveValue(funcMatch[2]);
|
|
612
|
+
items = items.filter(item => String(item[attributeName] || "").startsWith(String(expectedValue)));
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Tenta match de operador infix: attr OP :val
|
|
617
|
+
const match = trimmedPart.match(/([^\s]+)\s*(=|>|<|>=|<=|BEGINS_WITH|BETWEEN)\s*([^\s]+)(?:\s+AND\s+([^\s]+))?/i);
|
|
606
618
|
if (match) {
|
|
607
619
|
const attrPlaceholder = match[1];
|
|
608
620
|
const operator = match[2].toUpperCase();
|
|
609
621
|
const valPlaceholder = match[3];
|
|
610
|
-
|
|
611
622
|
const attributeName = resolveAttributeName(attrPlaceholder);
|
|
612
623
|
const expectedValue = resolveValue(valPlaceholder);
|
|
613
624
|
|
|
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") {
|
|
625
|
+
if (operator === "=") items = items.filter(item => item[attributeName] === expectedValue);
|
|
626
|
+
else if (operator === ">") items = items.filter(item => item[attributeName] > expectedValue);
|
|
627
|
+
else if (operator === "<") items = items.filter(item => item[attributeName] < expectedValue);
|
|
628
|
+
else if (operator === ">=") items = items.filter(item => item[attributeName] >= expectedValue);
|
|
629
|
+
else if (operator === "<=") items = items.filter(item => item[attributeName] <= expectedValue);
|
|
630
|
+
else if (operator === "BEGINS_WITH") {
|
|
625
631
|
const val = expectedValue;
|
|
626
632
|
items = items.filter(item => String(item[attributeName] || "").startsWith(String(val)));
|
|
627
633
|
}
|
|
@@ -629,44 +635,148 @@ class DynamoDBSimulator {
|
|
|
629
635
|
}
|
|
630
636
|
}
|
|
631
637
|
|
|
632
|
-
|
|
638
|
+
// Ordenação (DynamoDB sempre ordena pela Sort Key)
|
|
639
|
+
let sortKey = table.rangeKey;
|
|
640
|
+
if (IndexName && table.globalSecondaryIndexes?.[IndexName]) {
|
|
641
|
+
sortKey = table.globalSecondaryIndexes[IndexName].rangeKey;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (sortKey) {
|
|
645
|
+
items.sort((a, b) => {
|
|
646
|
+
const valA = a[sortKey];
|
|
647
|
+
const valB = b[sortKey];
|
|
648
|
+
|
|
649
|
+
if (valA === valB) return 0;
|
|
650
|
+
if (valA === undefined || valA === null) return 1;
|
|
651
|
+
if (valB === undefined || valB === null) return -1;
|
|
652
|
+
|
|
653
|
+
let comparison = 0;
|
|
654
|
+
if (typeof valA === 'number' && typeof valB === 'number') {
|
|
655
|
+
comparison = valA - valB;
|
|
656
|
+
} else {
|
|
657
|
+
comparison = String(valA).localeCompare(String(valB));
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return ScanIndexForward ? comparison : -comparison;
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const scannedCount = items.length;
|
|
665
|
+
|
|
666
|
+
// Aplica FilterExpression se existir
|
|
667
|
+
if (FilterExpression) {
|
|
668
|
+
items = this.applyFilter(items, FilterExpression, ExpressionAttributeValues, ExpressionAttributeNames, table);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const totalMatchingCount = items.length;
|
|
672
|
+
|
|
673
|
+
// Apply Pagination (ExclusiveStartKey)
|
|
674
|
+
if (ExclusiveStartKey) {
|
|
675
|
+
const startKeyStr = this.getItemKeyFromKeys(ExclusiveStartKey, table);
|
|
676
|
+
const startIndex = items.findIndex(item => this.getItemKey(item, table) === startKeyStr);
|
|
677
|
+
if (startIndex !== -1) {
|
|
678
|
+
items = items.slice(startIndex + 1);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Apply Limit
|
|
683
|
+
let lastEvaluatedKey = null;
|
|
684
|
+
if (Limit && items.length > Limit) {
|
|
685
|
+
const lastItem = items[Limit - 1];
|
|
686
|
+
lastEvaluatedKey = this.marshallItem(lastItem, table);
|
|
687
|
+
items = items.slice(0, Limit);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
let marshalledItems = items.map((item) => this.marshallItem(item, table));
|
|
691
|
+
|
|
692
|
+
// Apply Projection
|
|
693
|
+
if (ProjectionExpression) {
|
|
694
|
+
marshalledItems = this.applyProjection(marshalledItems, ProjectionExpression, ExpressionAttributeNames);
|
|
695
|
+
}
|
|
633
696
|
|
|
634
697
|
return {
|
|
635
698
|
Items: marshalledItems,
|
|
636
699
|
Count: marshalledItems.length,
|
|
637
|
-
ScannedCount:
|
|
700
|
+
ScannedCount: scannedCount,
|
|
701
|
+
LastEvaluatedKey: lastEvaluatedKey || undefined
|
|
638
702
|
};
|
|
639
703
|
}
|
|
640
704
|
|
|
641
705
|
scan(params) {
|
|
642
|
-
const {
|
|
706
|
+
const {
|
|
707
|
+
TableName,
|
|
708
|
+
FilterExpression,
|
|
709
|
+
ExpressionAttributeValues,
|
|
710
|
+
ExpressionAttributeNames = {},
|
|
711
|
+
Limit,
|
|
712
|
+
ExclusiveStartKey,
|
|
713
|
+
ProjectionExpression
|
|
714
|
+
} = params;
|
|
643
715
|
const table = this.tables.get(TableName);
|
|
644
716
|
|
|
645
717
|
if (!table) {
|
|
646
718
|
throw new Error(`Table ${TableName} does not exist`);
|
|
647
719
|
}
|
|
648
720
|
|
|
649
|
-
|
|
721
|
+
const allItems = this.store.read(TableName);
|
|
722
|
+
let items = allItems;
|
|
723
|
+
const scannedCount = items.length;
|
|
650
724
|
|
|
651
725
|
// Aplica filtro se existir
|
|
652
726
|
if (FilterExpression) {
|
|
653
727
|
items = this.applyFilter(items, FilterExpression, ExpressionAttributeValues, ExpressionAttributeNames, table);
|
|
654
728
|
}
|
|
655
729
|
|
|
656
|
-
//
|
|
730
|
+
// Apply Pagination (ExclusiveStartKey)
|
|
731
|
+
if (ExclusiveStartKey) {
|
|
732
|
+
const startKeyStr = this.getItemKeyFromKeys(ExclusiveStartKey, table);
|
|
733
|
+
const startIndex = items.findIndex(item => this.getItemKey(item, table) === startKeyStr);
|
|
734
|
+
if (startIndex !== -1) {
|
|
735
|
+
items = items.slice(startIndex + 1);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Apply Limit
|
|
740
|
+
let lastEvaluatedKey = null;
|
|
657
741
|
if (Limit && items.length > Limit) {
|
|
742
|
+
const lastItem = items[Limit - 1];
|
|
743
|
+
lastEvaluatedKey = this.marshallItem(lastItem, table);
|
|
658
744
|
items = items.slice(0, Limit);
|
|
659
745
|
}
|
|
660
746
|
|
|
661
|
-
|
|
747
|
+
let marshalledItems = items.map((item) => this.marshallItem(item, table));
|
|
748
|
+
|
|
749
|
+
// Apply Projection
|
|
750
|
+
if (ProjectionExpression) {
|
|
751
|
+
marshalledItems = this.applyProjection(marshalledItems, ProjectionExpression, ExpressionAttributeNames);
|
|
752
|
+
}
|
|
662
753
|
|
|
663
754
|
return {
|
|
664
755
|
Items: marshalledItems,
|
|
665
756
|
Count: marshalledItems.length,
|
|
666
|
-
ScannedCount:
|
|
757
|
+
ScannedCount: scannedCount,
|
|
758
|
+
LastEvaluatedKey: lastEvaluatedKey || undefined
|
|
667
759
|
};
|
|
668
760
|
}
|
|
669
761
|
|
|
762
|
+
applyProjection(items, expression, names = {}) {
|
|
763
|
+
const projectedAttrs = expression.split(',').map(s => s.trim()).filter(Boolean);
|
|
764
|
+
const resolvedAttrs = projectedAttrs.map(attr => {
|
|
765
|
+
if (attr.startsWith("#")) return names[attr] || attr;
|
|
766
|
+
return attr;
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
return items.map(item => {
|
|
770
|
+
const newItem = {};
|
|
771
|
+
resolvedAttrs.forEach(attr => {
|
|
772
|
+
if (item[attr] !== undefined) {
|
|
773
|
+
newItem[attr] = item[attr];
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
return newItem;
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
670
780
|
|
|
671
781
|
// Métodos auxiliares
|
|
672
782
|
normalizeItem(item, table) {
|
|
@@ -748,8 +858,8 @@ class DynamoDBSimulator {
|
|
|
748
858
|
const [path, valueExpr] = assignment.split("=").map((s) => s.trim());
|
|
749
859
|
const attributeName = nameMap[path] || path.replace(/#/g, "");
|
|
750
860
|
const rawValue = valueMap[valueExpr];
|
|
751
|
-
|
|
752
|
-
item[attributeName] =
|
|
861
|
+
// Usa normalizeValue para garantir o mesmo formato que o putItem
|
|
862
|
+
item[attributeName] = this.normalizeValue(rawValue, table);
|
|
753
863
|
}
|
|
754
864
|
}
|
|
755
865
|
|
|
@@ -761,7 +871,8 @@ class DynamoDBSimulator {
|
|
|
761
871
|
const parts = assignment.split(/\s+/);
|
|
762
872
|
const attributeName = nameMap[parts[0]] || parts[0].replace(/#/g, "");
|
|
763
873
|
const rawValue = valueMap[parts[1]];
|
|
764
|
-
|
|
874
|
+
// Usa normalizeValue para garantir o mesmo formato que o putItem
|
|
875
|
+
const delta = this.normalizeValue(rawValue, table);
|
|
765
876
|
const current = item[attributeName];
|
|
766
877
|
if (current === undefined || current === null) {
|
|
767
878
|
item[attributeName] = typeof delta === 'number' ? delta : parseFloat(delta) || 0;
|