@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
@@ -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.30",
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-01T13:34:46.765Z",
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: 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
  });
@@ -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;