@gugananuvem/aws-local-simulator 1.0.0 → 1.0.2
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 +192 -192
- package/bin/aws-local-simulator.js +62 -62
- package/package.json +7 -4
- package/src/config/config-loader.js +113 -0
- package/src/config/default-config.js +65 -0
- package/src/config/env-loader.js +67 -0
- package/src/index.js +130 -130
- package/src/index.mjs +124 -0
- package/src/server.js +219 -0
- package/src/services/apigateway/index.js +67 -0
- package/src/services/apigateway/server.js +435 -0
- package/src/services/apigateway/simulator.js +1252 -0
- package/src/services/cognito/index.js +66 -0
- package/src/services/cognito/server.js +229 -0
- package/src/services/cognito/simulator.js +848 -0
- package/src/services/dynamodb/index.js +71 -0
- package/src/services/dynamodb/server.js +122 -0
- package/src/services/dynamodb/simulator.js +614 -0
- package/src/services/eventbridge/index.js +85 -0
- package/src/services/index.js +19 -0
- package/src/services/lambda/handler-loader.js +173 -0
- package/src/services/lambda/index.js +73 -0
- package/src/services/lambda/route-registry.js +275 -0
- package/src/services/lambda/server.js +153 -0
- package/src/services/lambda/simulator.js +278 -0
- package/src/services/s3/index.js +70 -0
- package/src/services/s3/server.js +239 -0
- package/src/services/s3/simulator.js +740 -0
- package/src/services/sns/index.js +76 -0
- package/src/services/sqs/index.js +96 -0
- package/src/services/sqs/server.js +274 -0
- package/src/services/sqs/simulator.js +660 -0
- package/src/template/aws-config-template.js +88 -0
- package/src/template/aws-config-template.mjs +91 -0
- package/src/template/config-template.json +165 -0
- package/src/utils/aws-config.js +92 -0
- package/src/utils/local-store.js +68 -0
- package/src/utils/logger.js +60 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DynamoDB Simulator Core
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const LocalStore = require("../../utils/local-store");
|
|
6
|
+
const logger = require("../../utils/logger");
|
|
7
|
+
const crypto = require("crypto");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
class DynamoDBSimulator {
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
const dataDir = process.env.AWS_LOCAL_SIMULATOR_DATA_DIR || config.dataDir || "./.aws-local-simulator-data";
|
|
14
|
+
|
|
15
|
+
if (!dataDir) {
|
|
16
|
+
throw new Error("AWS_LOCAL_SIMULATOR_DATA_DIR not set");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
this.dataDir = path.join(dataDir, "dynamodb");
|
|
20
|
+
this.store = new LocalStore(this.dataDir);
|
|
21
|
+
this.tables = new Map();
|
|
22
|
+
}
|
|
23
|
+
async initialize() {
|
|
24
|
+
logger.debug("Inicializando DynamoDB Simulator...");
|
|
25
|
+
this.loadTables();
|
|
26
|
+
logger.debug(`✅ DynamoDB Simulator inicializado com ${this.tables.size} tabelas`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
loadTables() {
|
|
30
|
+
// Carrega tabelas da configuração
|
|
31
|
+
if (this.config.dynamodb?.tables) {
|
|
32
|
+
for (const tableDef of this.config.dynamodb.tables) {
|
|
33
|
+
this.createTable(tableDef);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Carrega tabelas existentes do disco
|
|
38
|
+
const savedTables = this.store.read("__tables__");
|
|
39
|
+
if (savedTables) {
|
|
40
|
+
for (const [name, definition] of Object.entries(savedTables)) {
|
|
41
|
+
if (!this.tables.has(name)) {
|
|
42
|
+
this.tables.set(name, definition);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
createTable(params) {
|
|
49
|
+
const { TableName, KeySchema, AttributeDefinitions, ProvisionedThroughput } = params;
|
|
50
|
+
|
|
51
|
+
if (this.tables.has(TableName)) {
|
|
52
|
+
logger.warn(`Tabela ${TableName} já existe`);
|
|
53
|
+
return { TableDescription: { TableName, TableStatus: "ACTIVE" } };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const hashKey = KeySchema.find((k) => k.KeyType === "HASH").AttributeName;
|
|
57
|
+
const rangeKey = KeySchema.find((k) => k.KeyType === "RANGE")?.AttributeName;
|
|
58
|
+
|
|
59
|
+
const attributeTypes = {};
|
|
60
|
+
AttributeDefinitions.forEach((attr) => {
|
|
61
|
+
attributeTypes[attr.AttributeName] = attr.AttributeType;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const table = {
|
|
65
|
+
name: TableName,
|
|
66
|
+
hashKey,
|
|
67
|
+
rangeKey,
|
|
68
|
+
attributeTypes,
|
|
69
|
+
createdAt: new Date().toISOString(),
|
|
70
|
+
itemCount: 0,
|
|
71
|
+
sizeBytes: 0,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
this.tables.set(TableName, table);
|
|
75
|
+
this.persistTables();
|
|
76
|
+
|
|
77
|
+
// Inicializa arquivo de dados
|
|
78
|
+
this.store.write(TableName, []);
|
|
79
|
+
|
|
80
|
+
logger.debug(`✅ Tabela criada: ${TableName}`);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
TableDescription: {
|
|
84
|
+
TableName,
|
|
85
|
+
TableStatus: "ACTIVE",
|
|
86
|
+
CreationDateTime: new Date().toISOString(),
|
|
87
|
+
KeySchema,
|
|
88
|
+
AttributeDefinitions,
|
|
89
|
+
ProvisionedThroughput: ProvisionedThroughput || {
|
|
90
|
+
ReadCapacityUnits: 5,
|
|
91
|
+
WriteCapacityUnits: 5,
|
|
92
|
+
},
|
|
93
|
+
ItemCount: 0,
|
|
94
|
+
TableSizeBytes: 0,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async handleRequest(target, params) {
|
|
100
|
+
const action = target.split(".")[1];
|
|
101
|
+
|
|
102
|
+
logger.verboso(`DynamoDB Action: ${action}`, params);
|
|
103
|
+
|
|
104
|
+
switch (action) {
|
|
105
|
+
case "CreateTable":
|
|
106
|
+
return this.createTable(params);
|
|
107
|
+
case "DescribeTable":
|
|
108
|
+
return this.describeTable(params.TableName);
|
|
109
|
+
case "ListTables":
|
|
110
|
+
return this.listTables(params);
|
|
111
|
+
case "DeleteTable":
|
|
112
|
+
return this.deleteTable(params);
|
|
113
|
+
case "PutItem":
|
|
114
|
+
return this.putItem(params);
|
|
115
|
+
case "GetItem":
|
|
116
|
+
return this.getItem(params);
|
|
117
|
+
case "UpdateItem":
|
|
118
|
+
return this.updateItem(params);
|
|
119
|
+
case "DeleteItem":
|
|
120
|
+
return this.deleteItem(params);
|
|
121
|
+
case "BatchWriteItem":
|
|
122
|
+
return this.batchWriteItem(params);
|
|
123
|
+
case "BatchGetItem":
|
|
124
|
+
return this.batchGetItem(params);
|
|
125
|
+
case "Query":
|
|
126
|
+
return this.query(params);
|
|
127
|
+
case "Scan":
|
|
128
|
+
return this.scan(params);
|
|
129
|
+
default:
|
|
130
|
+
throw new Error(`Unsupported action: ${action}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
describeTable(tableName) {
|
|
135
|
+
const table = this.tables.get(tableName);
|
|
136
|
+
if (!table) {
|
|
137
|
+
throw new Error(`Table ${tableName} does not exist`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const items = this.store.read(tableName);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
Table: {
|
|
144
|
+
TableName: table.name,
|
|
145
|
+
TableStatus: "ACTIVE",
|
|
146
|
+
CreationDateTime: table.createdAt,
|
|
147
|
+
KeySchema: [{ AttributeName: table.hashKey, KeyType: "HASH" }, ...(table.rangeKey ? [{ AttributeName: table.rangeKey, KeyType: "RANGE" }] : [])],
|
|
148
|
+
AttributeDefinitions: Object.entries(table.attributeTypes).map(([name, type]) => ({
|
|
149
|
+
AttributeName: name,
|
|
150
|
+
AttributeType: type,
|
|
151
|
+
})),
|
|
152
|
+
ItemCount: items.length,
|
|
153
|
+
TableSizeBytes: JSON.stringify(items).length,
|
|
154
|
+
ProvisionedThroughput: {
|
|
155
|
+
ReadCapacityUnits: 5,
|
|
156
|
+
WriteCapacityUnits: 5,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
listTables(params = {}) {
|
|
163
|
+
const tableNames = Array.from(this.tables.keys());
|
|
164
|
+
const { Limit = 100, ExclusiveStartTableName } = params;
|
|
165
|
+
|
|
166
|
+
let startIndex = 0;
|
|
167
|
+
if (ExclusiveStartTableName) {
|
|
168
|
+
const index = tableNames.indexOf(ExclusiveStartTableName);
|
|
169
|
+
if (index !== -1) startIndex = index + 1;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const result = tableNames.slice(startIndex, startIndex + Limit);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
TableNames: result,
|
|
176
|
+
LastEvaluatedTableName: result.length === Limit ? result[result.length - 1] : undefined,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
deleteTable(params) {
|
|
181
|
+
const { TableName } = params;
|
|
182
|
+
|
|
183
|
+
if (!this.tables.has(TableName)) {
|
|
184
|
+
throw new Error(`Table ${TableName} does not exist`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.tables.delete(TableName);
|
|
188
|
+
this.store.delete(TableName);
|
|
189
|
+
this.persistTables();
|
|
190
|
+
|
|
191
|
+
return { TableDescription: { TableName, TableStatus: "DELETING" } };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
putItem(params) {
|
|
195
|
+
const { TableName, Item, ReturnValues = "NONE" } = params;
|
|
196
|
+
const table = this.tables.get(TableName);
|
|
197
|
+
|
|
198
|
+
if (!table) {
|
|
199
|
+
throw new Error(`Table ${TableName} does not exist`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Normaliza o item
|
|
203
|
+
const normalizedItem = this.normalizeItem(Item, table);
|
|
204
|
+
normalizedItem._createdAt = normalizedItem._createdAt || new Date().toISOString();
|
|
205
|
+
normalizedItem._updatedAt = new Date().toISOString();
|
|
206
|
+
|
|
207
|
+
// Carrega dados existentes
|
|
208
|
+
let items = this.store.read(TableName);
|
|
209
|
+
const itemKey = this.getItemKey(normalizedItem, table);
|
|
210
|
+
|
|
211
|
+
// Encontra e substitui ou adiciona
|
|
212
|
+
const existingIndex = items.findIndex((item) => this.getItemKey(item, table) === itemKey);
|
|
213
|
+
|
|
214
|
+
let oldItem = null;
|
|
215
|
+
if (existingIndex !== -1) {
|
|
216
|
+
oldItem = { ...items[existingIndex] };
|
|
217
|
+
items[existingIndex] = normalizedItem;
|
|
218
|
+
} else {
|
|
219
|
+
items.push(normalizedItem);
|
|
220
|
+
table.itemCount++;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Salva no store
|
|
224
|
+
this.store.write(TableName, items);
|
|
225
|
+
this.persistTables();
|
|
226
|
+
|
|
227
|
+
logger.verboso(`PutItem: ${TableName}/${itemKey}`);
|
|
228
|
+
|
|
229
|
+
const response = {};
|
|
230
|
+
if (ReturnValues === "ALL_OLD" && oldItem) {
|
|
231
|
+
response.Attributes = this.marshallItem(oldItem, table);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return response;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
getItem(params) {
|
|
238
|
+
const { TableName, Key } = params;
|
|
239
|
+
const table = this.tables.get(TableName);
|
|
240
|
+
|
|
241
|
+
if (!table) {
|
|
242
|
+
throw new Error(`Table ${TableName} does not exist`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const items = this.store.read(TableName);
|
|
246
|
+
const itemKey = this.getItemKeyFromKeys(Key, table);
|
|
247
|
+
|
|
248
|
+
const item = items.find((item) => this.getItemKey(item, table) === itemKey);
|
|
249
|
+
|
|
250
|
+
logger.verboso(`GetItem: ${TableName}/${itemKey} - ${item ? "found" : "not found"}`);
|
|
251
|
+
|
|
252
|
+
return item ? { Item: this.marshallItem(item, table) } : {};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
updateItem(params) {
|
|
256
|
+
const { TableName, Key, UpdateExpression, ExpressionAttributeNames = {}, ExpressionAttributeValues = {}, ReturnValues = "NONE" } = params;
|
|
257
|
+
const table = this.tables.get(TableName);
|
|
258
|
+
|
|
259
|
+
if (!table) {
|
|
260
|
+
throw new Error(`Table ${TableName} does not exist`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Busca o item atual
|
|
264
|
+
const items = this.store.read(TableName);
|
|
265
|
+
const itemKey = this.getItemKeyFromKeys(Key, table);
|
|
266
|
+
const index = items.findIndex((item) => this.getItemKey(item, table) === itemKey);
|
|
267
|
+
|
|
268
|
+
if (index === -1) {
|
|
269
|
+
throw new Error(`Item not found in ${TableName}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const currentItem = items[index];
|
|
273
|
+
const updatedItem = { ...currentItem };
|
|
274
|
+
updatedItem._updatedAt = new Date().toISOString();
|
|
275
|
+
|
|
276
|
+
// Processa a UpdateExpression
|
|
277
|
+
if (UpdateExpression) {
|
|
278
|
+
this.processUpdateExpression(updatedItem, UpdateExpression, ExpressionAttributeNames, ExpressionAttributeValues, table);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Salva o item atualizado
|
|
282
|
+
const oldItem = { ...items[index] };
|
|
283
|
+
items[index] = updatedItem;
|
|
284
|
+
this.store.write(TableName, items);
|
|
285
|
+
|
|
286
|
+
logger.verboso(`UpdateItem: ${TableName}/${itemKey}`);
|
|
287
|
+
|
|
288
|
+
const response = {};
|
|
289
|
+
switch (ReturnValues) {
|
|
290
|
+
case "ALL_OLD":
|
|
291
|
+
response.Attributes = this.marshallItem(oldItem, table);
|
|
292
|
+
break;
|
|
293
|
+
case "ALL_NEW":
|
|
294
|
+
response.Attributes = this.marshallItem(updatedItem, table);
|
|
295
|
+
break;
|
|
296
|
+
default:
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return response;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
deleteItem(params) {
|
|
304
|
+
const { TableName, Key, ReturnValues = "NONE" } = params;
|
|
305
|
+
const table = this.tables.get(TableName);
|
|
306
|
+
|
|
307
|
+
if (!table) {
|
|
308
|
+
throw new Error(`Table ${TableName} does not exist`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const items = this.store.read(TableName);
|
|
312
|
+
const itemKey = this.getItemKeyFromKeys(Key, table);
|
|
313
|
+
const index = items.findIndex((item) => this.getItemKey(item, table) === itemKey);
|
|
314
|
+
|
|
315
|
+
if (index === -1) {
|
|
316
|
+
return {};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const oldItem = { ...items[index] };
|
|
320
|
+
items.splice(index, 1);
|
|
321
|
+
this.store.write(TableName, items);
|
|
322
|
+
table.itemCount--;
|
|
323
|
+
this.persistTables();
|
|
324
|
+
|
|
325
|
+
logger.verboso(`DeleteItem: ${TableName}/${itemKey}`);
|
|
326
|
+
|
|
327
|
+
const response = {};
|
|
328
|
+
if (ReturnValues === "ALL_OLD") {
|
|
329
|
+
response.Attributes = this.marshallItem(oldItem, table);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return response;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
batchWriteItem(params) {
|
|
336
|
+
const { RequestItems } = params;
|
|
337
|
+
const responses = {};
|
|
338
|
+
|
|
339
|
+
for (const [tableName, operations] of Object.entries(RequestItems)) {
|
|
340
|
+
const table = this.tables.get(tableName);
|
|
341
|
+
if (!table) continue;
|
|
342
|
+
|
|
343
|
+
let items = this.store.read(tableName);
|
|
344
|
+
const unprocessedItems = [];
|
|
345
|
+
|
|
346
|
+
for (const op of operations) {
|
|
347
|
+
if (op.PutRequest) {
|
|
348
|
+
const item = this.normalizeItem(op.PutRequest.Item, table);
|
|
349
|
+
const itemKey = this.getItemKey(item, table);
|
|
350
|
+
const index = items.findIndex((i) => this.getItemKey(i, table) === itemKey);
|
|
351
|
+
|
|
352
|
+
if (index !== -1) {
|
|
353
|
+
items[index] = item;
|
|
354
|
+
} else {
|
|
355
|
+
items.push(item);
|
|
356
|
+
table.itemCount++;
|
|
357
|
+
}
|
|
358
|
+
} else if (op.DeleteRequest) {
|
|
359
|
+
const key = op.DeleteRequest.Key;
|
|
360
|
+
const itemKey = this.getItemKeyFromKeys(key, table);
|
|
361
|
+
const index = items.findIndex((i) => this.getItemKey(i, table) === itemKey);
|
|
362
|
+
|
|
363
|
+
if (index !== -1) {
|
|
364
|
+
items.splice(index, 1);
|
|
365
|
+
table.itemCount--;
|
|
366
|
+
} else {
|
|
367
|
+
unprocessedItems.push(op);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
this.store.write(tableName, items);
|
|
373
|
+
responses[tableName] = { UnprocessedItems: unprocessedItems };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
this.persistTables();
|
|
377
|
+
|
|
378
|
+
return { UnprocessedItems: responses };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
batchGetItem(params) {
|
|
382
|
+
const { RequestItems } = params;
|
|
383
|
+
const responses = {};
|
|
384
|
+
|
|
385
|
+
for (const [tableName, request] of Object.entries(RequestItems)) {
|
|
386
|
+
const table = this.tables.get(tableName);
|
|
387
|
+
if (!table) continue;
|
|
388
|
+
|
|
389
|
+
const items = this.store.read(tableName);
|
|
390
|
+
const { Keys } = request;
|
|
391
|
+
const foundItems = [];
|
|
392
|
+
|
|
393
|
+
for (const key of Keys) {
|
|
394
|
+
const itemKey = this.getItemKeyFromKeys(key, table);
|
|
395
|
+
const item = items.find((i) => this.getItemKey(i, table) === itemKey);
|
|
396
|
+
if (item) {
|
|
397
|
+
foundItems.push(this.marshallItem(item, table));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
responses[tableName] = { Items: foundItems };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return { Responses: responses };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
query(params) {
|
|
408
|
+
const { TableName, KeyConditionExpression, ExpressionAttributeValues, IndexName } = params;
|
|
409
|
+
const table = this.tables.get(TableName);
|
|
410
|
+
|
|
411
|
+
if (!table) {
|
|
412
|
+
throw new Error(`Table ${TableName} does not exist`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
let items = this.store.read(TableName);
|
|
416
|
+
|
|
417
|
+
// Filtra pela chave de partição
|
|
418
|
+
const hashKey = table.hashKey;
|
|
419
|
+
const hashValueMatch = KeyConditionExpression.match(new RegExp(`${hashKey}\\s*=\\s*([^\\s]+)`));
|
|
420
|
+
|
|
421
|
+
if (hashValueMatch) {
|
|
422
|
+
const hashValuePlaceholder = hashValueMatch[1];
|
|
423
|
+
const hashValue = ExpressionAttributeValues[hashValuePlaceholder];
|
|
424
|
+
items = items.filter((item) => item[hashKey] === hashValue);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Filtra pela chave de ordenação se existir
|
|
428
|
+
if (table.rangeKey) {
|
|
429
|
+
const rangeKey = table.rangeKey;
|
|
430
|
+
const rangeConditionMatch = KeyConditionExpression.match(new RegExp(`${rangeKey}\\s*(=|>|<|>=|<=)\\s*([^\\s]+)`));
|
|
431
|
+
|
|
432
|
+
if (rangeConditionMatch) {
|
|
433
|
+
const operator = rangeConditionMatch[1];
|
|
434
|
+
const rangeValuePlaceholder = rangeConditionMatch[2];
|
|
435
|
+
const rangeValue = ExpressionAttributeValues[rangeValuePlaceholder];
|
|
436
|
+
|
|
437
|
+
items = items.filter((item) => {
|
|
438
|
+
const itemValue = item[rangeKey];
|
|
439
|
+
switch (operator) {
|
|
440
|
+
case "=":
|
|
441
|
+
return itemValue === rangeValue;
|
|
442
|
+
case ">":
|
|
443
|
+
return itemValue > rangeValue;
|
|
444
|
+
case "<":
|
|
445
|
+
return itemValue < rangeValue;
|
|
446
|
+
case ">=":
|
|
447
|
+
return itemValue >= rangeValue;
|
|
448
|
+
case "<=":
|
|
449
|
+
return itemValue <= rangeValue;
|
|
450
|
+
default:
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const marshalledItems = items.map((item) => this.marshallItem(item, table));
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
Items: marshalledItems,
|
|
461
|
+
Count: marshalledItems.length,
|
|
462
|
+
ScannedCount: items.length,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
scan(params) {
|
|
467
|
+
const { TableName, FilterExpression, ExpressionAttributeValues, Limit } = params;
|
|
468
|
+
const table = this.tables.get(TableName);
|
|
469
|
+
|
|
470
|
+
if (!table) {
|
|
471
|
+
throw new Error(`Table ${TableName} does not exist`);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
let items = this.store.read(TableName);
|
|
475
|
+
|
|
476
|
+
// Aplica filtro se existir
|
|
477
|
+
if (FilterExpression) {
|
|
478
|
+
items = this.applyFilter(items, FilterExpression, ExpressionAttributeValues, table);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Aplica limite
|
|
482
|
+
if (Limit && items.length > Limit) {
|
|
483
|
+
items = items.slice(0, Limit);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const marshalledItems = items.map((item) => this.marshallItem(item, table));
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
Items: marshalledItems,
|
|
490
|
+
Count: marshalledItems.length,
|
|
491
|
+
ScannedCount: items.length,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Métodos auxiliares
|
|
496
|
+
normalizeItem(item, table) {
|
|
497
|
+
const normalized = { ...item };
|
|
498
|
+
|
|
499
|
+
// Remove os tipos do DynamoDB (S, N, etc)
|
|
500
|
+
for (const [key, value] of Object.entries(normalized)) {
|
|
501
|
+
if (value && typeof value === "object") {
|
|
502
|
+
if (value.S !== undefined) normalized[key] = value.S;
|
|
503
|
+
else if (value.N !== undefined) normalized[key] = parseFloat(value.N);
|
|
504
|
+
else if (value.BOOL !== undefined) normalized[key] = value.BOOL;
|
|
505
|
+
else if (value.L !== undefined) normalized[key] = value.L.map((v) => this.normalizeItem(v, table));
|
|
506
|
+
else if (value.M !== undefined) normalized[key] = this.normalizeItem(value.M, table);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return normalized;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
marshallItem(item, table) {
|
|
514
|
+
const marshalled = {};
|
|
515
|
+
|
|
516
|
+
for (const [key, value] of Object.entries(item)) {
|
|
517
|
+
if (key.startsWith("_")) continue; // Pula campos internos
|
|
518
|
+
|
|
519
|
+
const type = table.attributeTypes[key];
|
|
520
|
+
if (type === "S") {
|
|
521
|
+
marshalled[key] = { S: String(value) };
|
|
522
|
+
} else if (type === "N") {
|
|
523
|
+
marshalled[key] = { N: String(value) };
|
|
524
|
+
} else if (type === "BOOL") {
|
|
525
|
+
marshalled[key] = { BOOL: Boolean(value) };
|
|
526
|
+
} else if (Array.isArray(value)) {
|
|
527
|
+
marshalled[key] = { L: value.map((v) => ({ S: String(v) })) };
|
|
528
|
+
} else if (typeof value === "object") {
|
|
529
|
+
marshalled[key] = { M: this.marshallItem(value, table) };
|
|
530
|
+
} else {
|
|
531
|
+
marshalled[key] = { S: String(value) };
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return marshalled;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
getItemKey(item, table) {
|
|
539
|
+
const hashValue = item[table.hashKey];
|
|
540
|
+
const rangeValue = table.rangeKey ? item[table.rangeKey] : null;
|
|
541
|
+
return rangeValue ? `${hashValue}|${rangeValue}` : String(hashValue);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
getItemKeyFromKeys(keys, table) {
|
|
545
|
+
const hashValue = keys[table.hashKey];
|
|
546
|
+
const rangeValue = table.rangeKey ? keys[table.rangeKey] : null;
|
|
547
|
+
return rangeValue ? `${hashValue}|${rangeValue}` : String(hashValue);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
processUpdateExpression(item, expression, nameMap, valueMap, table) {
|
|
551
|
+
// Implementação simplificada - expandir conforme necessário
|
|
552
|
+
const setMatch = expression.match(/SET\s+([^]+?)(?=\s+(?:REMOVE|ADD|DELETE)|\s*$)/i);
|
|
553
|
+
|
|
554
|
+
if (setMatch) {
|
|
555
|
+
const assignments = setMatch[1].split(",").map((a) => a.trim());
|
|
556
|
+
|
|
557
|
+
for (const assignment of assignments) {
|
|
558
|
+
const [path, valueExpr] = assignment.split("=").map((s) => s.trim());
|
|
559
|
+
const attributeName = path.replace(/#/g, "");
|
|
560
|
+
const value = valueMap[valueExpr];
|
|
561
|
+
|
|
562
|
+
item[attributeName] = value;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
applyFilter(items, expression, values, table) {
|
|
568
|
+
// Implementação simplificada
|
|
569
|
+
return items.filter((item) => {
|
|
570
|
+
const match = expression.match(/([^\s]+)\s*=\s*([^\s]+)/);
|
|
571
|
+
if (match) {
|
|
572
|
+
const [, attribute, placeholder] = match;
|
|
573
|
+
const expectedValue = values[placeholder];
|
|
574
|
+
const actualValue = item[attribute];
|
|
575
|
+
return actualValue === expectedValue;
|
|
576
|
+
}
|
|
577
|
+
return true;
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
persistTables() {
|
|
582
|
+
const tablesObj = {};
|
|
583
|
+
for (const [name, table] of this.tables.entries()) {
|
|
584
|
+
tablesObj[name] = table;
|
|
585
|
+
}
|
|
586
|
+
this.store.write("__tables__", tablesObj);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async reset() {
|
|
590
|
+
for (const [tableName] of this.tables) {
|
|
591
|
+
this.store.write(tableName, []);
|
|
592
|
+
}
|
|
593
|
+
logger.debug("DynamoDB: Todos os dados resetados");
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
getTablesCount() {
|
|
597
|
+
return this.tables.size;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
getTotalItems() {
|
|
601
|
+
let total = 0;
|
|
602
|
+
for (const [tableName] of this.tables) {
|
|
603
|
+
const items = this.store.read(tableName);
|
|
604
|
+
total += items.length;
|
|
605
|
+
}
|
|
606
|
+
return total;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
listTables() {
|
|
610
|
+
return { TableNames: Array.from(this.tables.keys()) };
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
module.exports = DynamoDBSimulator;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventBridge Service - Ponto de entrada (Stub para implementação futura)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const logger = require('../../utils/logger');
|
|
6
|
+
|
|
7
|
+
class EventBridgeService {
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.name = 'eventbridge';
|
|
11
|
+
this.port = config.ports.eventbridge;
|
|
12
|
+
this.isRunning = false;
|
|
13
|
+
this.buses = new Map();
|
|
14
|
+
this.events = [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async initialize() {
|
|
18
|
+
logger.debug(`Inicializando EventBridge Service na porta ${this.port}...`);
|
|
19
|
+
logger.warn('⚠️ EventBridge Service ainda não está completamente implementado');
|
|
20
|
+
|
|
21
|
+
// TODO: Implementar EventBridge simulator
|
|
22
|
+
this.buses = new Map();
|
|
23
|
+
this.events = [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async start() {
|
|
27
|
+
if (this.isRunning) return;
|
|
28
|
+
|
|
29
|
+
// TODO: Iniciar servidor HTTP para EventBridge
|
|
30
|
+
this.isRunning = true;
|
|
31
|
+
logger.info(`🎯 EventBridge Service stub rodando (porta ${this.port}) - Implementação em breve`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async stop() {
|
|
35
|
+
if (!this.isRunning) return;
|
|
36
|
+
this.isRunning = false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async reset() {
|
|
40
|
+
this.buses.clear();
|
|
41
|
+
this.events = [];
|
|
42
|
+
logger.debug('EventBridge: Todos os dados resetados');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getStatus() {
|
|
46
|
+
return {
|
|
47
|
+
running: this.isRunning,
|
|
48
|
+
port: this.port,
|
|
49
|
+
endpoint: `http://localhost:${this.port}`,
|
|
50
|
+
implemented: false,
|
|
51
|
+
busesCount: this.buses.size,
|
|
52
|
+
eventsCount: this.events.length
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Métodos stub para compatibilidade
|
|
57
|
+
async createEventBus(name) {
|
|
58
|
+
if (!this.buses.has(name)) {
|
|
59
|
+
this.buses.set(name, {
|
|
60
|
+
name,
|
|
61
|
+
arn: `arn:aws:events:local:000000000000:event-bus/${name}`,
|
|
62
|
+
createdAt: new Date().toISOString()
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return this.buses.get(name);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async putEvents(entries) {
|
|
69
|
+
const results = [];
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
const eventId = Math.random().toString(36).substring(7);
|
|
72
|
+
this.events.push({
|
|
73
|
+
...entry,
|
|
74
|
+
eventId,
|
|
75
|
+
time: new Date().toISOString(),
|
|
76
|
+
receivedAt: new Date().toISOString()
|
|
77
|
+
});
|
|
78
|
+
results.push({ EventId: eventId });
|
|
79
|
+
logger.verboso(`EventBridge: Event ${eventId} published to ${entry.EventBusName || 'default'}`);
|
|
80
|
+
}
|
|
81
|
+
return { Entries: results, FailedEntryCount: 0 };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = EventBridgeService;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barrel export para todos os serviços
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const DynamoDBService = require('./dynamodb');
|
|
6
|
+
const S3Service = require('./s3');
|
|
7
|
+
const SQSService = require('./sqs');
|
|
8
|
+
const LambdaService = require('./lambda');
|
|
9
|
+
const SNSService = require('./sns');
|
|
10
|
+
const EventBridgeService = require('./eventbridge');
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
DynamoDBService,
|
|
14
|
+
S3Service,
|
|
15
|
+
SQSService,
|
|
16
|
+
LambdaService,
|
|
17
|
+
SNSService,
|
|
18
|
+
EventBridgeService
|
|
19
|
+
};
|