@gugananuvem/aws-local-simulator 1.0.16 → 1.0.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gugananuvem/aws-local-simulator",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "Simulador local completo para serviços AWS",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -120,6 +120,6 @@
120
120
  "optional": true
121
121
  }
122
122
  },
123
- "buildDate": "2026-04-20T18:31:32.841Z",
123
+ "buildDate": "2026-04-21T01:06:51.838Z",
124
124
  "published": true
125
125
  }
@@ -20,7 +20,10 @@ class DynamoDBServer {
20
20
  setupMiddlewares() {
21
21
  this.app.use(cors());
22
22
  this.app.use(express.json({
23
- type: 'application/x-amz-json-1.0'
23
+ type: (req) => {
24
+ const ct = req.headers['content-type'] || '';
25
+ return ct.includes('application/x-amz-json-1.0') || ct.includes('application/json');
26
+ }
24
27
  }));
25
28
 
26
29
  // Logging de requisições
@@ -53,6 +56,8 @@ class DynamoDBServer {
53
56
  return res.status(400).json({ message: 'Missing X-Amz-Target header' });
54
57
  }
55
58
 
59
+ logger.debug(`DynamoDB target=${target} content-type=${req.headers['content-type']} body=${JSON.stringify(req.body)}`);
60
+
56
61
  try {
57
62
  const result = await this.simulator.handleRequest(target, req.body);
58
63
  res.json(result);
@@ -29,26 +29,24 @@ class DynamoDBSimulator {
29
29
  }
30
30
 
31
31
  loadTables() {
32
- // Carrega tabelas da configuração
33
- if (this.config.dynamodb?.tables) {
34
- for (const tableDef of this.config.dynamodb.tables) {
35
- this.createTable(tableDef);
36
- }
37
- }
38
-
39
- // Carrega tabelas existentes do disco
32
+ // Carrega tabelas existentes do disco PRIMEIRO para evitar sobrescrever definições persistidas
40
33
  const savedTables = this.store.read("__tables__");
41
34
  if (savedTables) {
42
35
  for (const [name, definition] of Object.entries(savedTables)) {
43
- if (!this.tables.has(name)) {
44
- this.tables.set(name, definition);
45
- }
36
+ this.tables.set(name, definition);
37
+ }
38
+ }
39
+
40
+ // Cria tabelas da configuração apenas se ainda não existirem no disco
41
+ if (this.config.dynamodb?.tables) {
42
+ for (const tableDef of this.config.dynamodb.tables) {
43
+ this.createTable(tableDef);
46
44
  }
47
45
  }
48
46
  }
49
47
 
50
48
  createTable(params) {
51
- const { TableName, KeySchema, AttributeDefinitions, ProvisionedThroughput } = params;
49
+ const { TableName, KeySchema, AttributeDefinitions, ProvisionedThroughput, GlobalSecondaryIndexes } = params;
52
50
 
53
51
  if (this.tables.has(TableName)) {
54
52
  logger.warn(`Tabela ${TableName} já existe`);
@@ -63,11 +61,21 @@ class DynamoDBSimulator {
63
61
  attributeTypes[attr.AttributeName] = attr.AttributeType;
64
62
  });
65
63
 
64
+ const globalSecondaryIndexes = {};
65
+ if (GlobalSecondaryIndexes) {
66
+ for (const gsi of GlobalSecondaryIndexes) {
67
+ const gsiHashKey = gsi.KeySchema.find((k) => k.KeyType === "HASH").AttributeName;
68
+ const gsiRangeKey = gsi.KeySchema.find((k) => k.KeyType === "RANGE")?.AttributeName;
69
+ globalSecondaryIndexes[gsi.IndexName] = { hashKey: gsiHashKey, rangeKey: gsiRangeKey };
70
+ }
71
+ }
72
+
66
73
  const table = {
67
74
  name: TableName,
68
75
  hashKey,
69
76
  rangeKey,
70
77
  attributeTypes,
78
+ globalSecondaryIndexes,
71
79
  createdAt: new Date().toISOString(),
72
80
  itemCount: 0,
73
81
  sizeBytes: 0,
@@ -270,13 +278,27 @@ class DynamoDBSimulator {
270
278
  throw new Error(`Table ${TableName} does not exist`);
271
279
  }
272
280
 
273
- // Busca o item atual
281
+ // Busca o item atual (upsert: cria se não existir, como o DynamoDB real)
274
282
  const items = this.store.read(TableName);
275
283
  const itemKey = this.getItemKeyFromKeys(Key, table);
276
284
  const index = items.findIndex((item) => this.getItemKey(item, table) === itemKey);
277
285
 
286
+ // Se não existe, cria um novo item com as chaves fornecidas
278
287
  if (index === -1) {
279
- throw new Error(`Item not found in ${TableName}`);
288
+ const newItem = this.normalizeItem(Key, table);
289
+ newItem._createdAt = new Date().toISOString();
290
+ newItem._updatedAt = new Date().toISOString();
291
+ if (UpdateExpression) {
292
+ this.processUpdateExpression(newItem, UpdateExpression, ExpressionAttributeNames, ExpressionAttributeValues, table);
293
+ }
294
+ items.push(newItem);
295
+ this.store.write(TableName, items);
296
+ logger.verboso(`UpdateItem (upsert): ${TableName}/${itemKey}`);
297
+ const response = {};
298
+ if (ReturnValues === "ALL_NEW" || ReturnValues === "UPDATED_NEW") {
299
+ response.Attributes = this.marshallItem(newItem, table);
300
+ }
301
+ return response;
280
302
  }
281
303
 
282
304
  const currentItem = items[index];
@@ -346,6 +368,11 @@ class DynamoDBSimulator {
346
368
  const { RequestItems } = params;
347
369
  const responses = {};
348
370
 
371
+ if (!RequestItems) {
372
+ logger.debug(`[DEBUG batchWriteItem] params recebido: ${JSON.stringify(params)}`);
373
+ throw new Error(`RequestItems is required for BatchWriteItem. Params received: ${JSON.stringify(params)}`);
374
+ }
375
+
349
376
  for (const [tableName, operations] of Object.entries(RequestItems)) {
350
377
  const table = this.tables.get(tableName);
351
378
  if (!table) continue;
@@ -424,8 +451,24 @@ class DynamoDBSimulator {
424
451
 
425
452
  let items = this.store.read(TableName);
426
453
 
454
+ // Resolve hash key e range key: usa GSI se IndexName estiver presente, caso contrário usa a tabela principal
455
+ let hashKey;
456
+ let rangeKey;
457
+
458
+ if (IndexName != null) {
459
+ const gsiDefs = table.globalSecondaryIndexes || {};
460
+ const gsi = gsiDefs[IndexName];
461
+ if (!gsi) {
462
+ throw new Error(`GSI "${IndexName}" not found on table "${TableName}"`);
463
+ }
464
+ hashKey = gsi.hashKey;
465
+ rangeKey = gsi.rangeKey;
466
+ } else {
467
+ hashKey = table.hashKey;
468
+ rangeKey = table.rangeKey;
469
+ }
470
+
427
471
  // Filtra pela chave de partição
428
- const hashKey = table.hashKey;
429
472
  const hashValueMatch = KeyConditionExpression.match(new RegExp(`${hashKey}\\s*=\\s*([^\\s]+)`));
430
473
 
431
474
  if (hashValueMatch) {
@@ -436,8 +479,7 @@ class DynamoDBSimulator {
436
479
  }
437
480
 
438
481
  // Filtra pela chave de ordenação se existir
439
- if (table.rangeKey) {
440
- const rangeKey = table.rangeKey;
482
+ if (rangeKey) {
441
483
  const rangeConditionMatch = KeyConditionExpression.match(new RegExp(`${rangeKey}\\s*(=|>|<|>=|<=)\\s*([^\\s]+)`));
442
484
 
443
485
  if (rangeConditionMatch) {
@@ -562,21 +604,46 @@ class DynamoDBSimulator {
562
604
  }
563
605
 
564
606
  processUpdateExpression(item, expression, nameMap, valueMap, table) {
565
- // Implementação simplificada - expandir conforme necessário
566
- const setMatch = expression.match(/SET\s+([^]+?)(?=\s+(?:REMOVE|ADD|DELETE)|\s*$)/i);
567
-
607
+ // SET clause
608
+ const setMatch = expression.match(/SET\s+([^]+?)(?=\s+(?:REMOVE|ADD|DELETE)\s|\s*$)/i);
568
609
  if (setMatch) {
569
610
  const assignments = setMatch[1].split(",").map((a) => a.trim());
570
-
571
611
  for (const assignment of assignments) {
572
612
  const [path, valueExpr] = assignment.split("=").map((s) => s.trim());
573
- const attributeName = path.replace(/#/g, "");
613
+ const attributeName = nameMap[path] || path.replace(/#/g, "");
574
614
  const rawValue = valueMap[valueExpr];
575
615
  const value = rawValue && typeof rawValue === 'object' ? Object.values(rawValue)[0] : rawValue;
576
-
577
616
  item[attributeName] = value;
578
617
  }
579
618
  }
619
+
620
+ // ADD clause — incrementa números ou adiciona a sets (upsert-friendly)
621
+ const addMatch = expression.match(/ADD\s+([^]+?)(?=\s+(?:SET|REMOVE|DELETE)\s|\s*$)/i);
622
+ if (addMatch) {
623
+ const assignments = addMatch[1].split(",").map((a) => a.trim());
624
+ for (const assignment of assignments) {
625
+ const parts = assignment.split(/\s+/);
626
+ const attributeName = nameMap[parts[0]] || parts[0].replace(/#/g, "");
627
+ const rawValue = valueMap[parts[1]];
628
+ const delta = rawValue && typeof rawValue === 'object' ? Object.values(rawValue)[0] : rawValue;
629
+ const current = item[attributeName];
630
+ if (current === undefined || current === null) {
631
+ item[attributeName] = typeof delta === 'number' ? delta : parseFloat(delta) || 0;
632
+ } else {
633
+ item[attributeName] = (parseFloat(current) || 0) + (parseFloat(delta) || 0);
634
+ }
635
+ }
636
+ }
637
+
638
+ // REMOVE clause
639
+ const removeMatch = expression.match(/REMOVE\s+([^]+?)(?=\s+(?:SET|ADD|DELETE)\s|\s*$)/i);
640
+ if (removeMatch) {
641
+ const attributes = removeMatch[1].split(",").map((a) => a.trim());
642
+ for (const attr of attributes) {
643
+ const attributeName = nameMap[attr] || attr.replace(/#/g, "");
644
+ delete item[attributeName];
645
+ }
646
+ }
580
647
  }
581
648
 
582
649
  applyFilter(items, expression, values, table) {
@@ -31,7 +31,7 @@ class LocalStore {
31
31
  if (!fs.existsSync(filePath)) return [];
32
32
 
33
33
  try {
34
- const content = fs.readFileSync(filePath, 'utf8');
34
+ const content = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
35
35
  return JSON.parse(content);
36
36
  } catch (error) {
37
37
  console.error(`Erro ao ler ${entity}:`, error);