@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.
|
|
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-
|
|
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:
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
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) {
|
package/src/utils/local-store.js
CHANGED
|
@@ -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);
|