@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 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.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-01T13:34:46.765Z",
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: 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
  });
@@ -1262,12 +1279,12 @@ class CognitoSimulator {
1262
1279
  return {
1263
1280
  User: {
1264
1281
  Username: user.Username,
1265
- UserAttributes: this._formatUserAttributesWithSub(user),
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
- TemporaryPassword: tempPassword,
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).toISOString(),
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 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,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 match = part.match(/([^\s]+)\s*(=|>|<|>=|<=|BEGINS_WITH|BETWEEN)\s*([^\s]+)(?:\s+AND\s+([^\s]+))?/i);
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
- 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") {
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
- const marshalledItems = items.map((item) => this.marshallItem(item, table));
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: items.length,
700
+ ScannedCount: scannedCount,
701
+ LastEvaluatedKey: lastEvaluatedKey || undefined
638
702
  };
639
703
  }
640
704
 
641
705
  scan(params) {
642
- const { TableName, FilterExpression, ExpressionAttributeValues, ExpressionAttributeNames = {}, Limit } = params;
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
- let items = this.store.read(TableName);
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
- // Aplica limite
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
- const marshalledItems = items.map((item) => this.marshallItem(item, table));
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: items.length,
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
- const value = rawValue && typeof rawValue === 'object' ? Object.values(rawValue)[0] : rawValue;
752
- item[attributeName] = value;
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
- const delta = rawValue && typeof rawValue === 'object' ? Object.values(rawValue)[0] : rawValue;
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;