@gugananuvem/aws-local-simulator 1.0.30 → 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
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-05-
|
|
69
|
+
"buildDate": "2026-05-05T10:06:53.890Z",
|
|
70
70
|
"published": true
|
|
71
71
|
}
|
|
@@ -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
|
});
|
|
@@ -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;
|