@gugananuvem/aws-local-simulator 1.0.31 → 1.0.34

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.
Files changed (79) hide show
  1. package/README.md +834 -834
  2. package/aws-config +153 -153
  3. package/bin/aws-local-simulator.js +63 -63
  4. package/package.json +3 -2
  5. package/src/config/config-loader.js +114 -114
  6. package/src/config/default-config.js +79 -79
  7. package/src/config/env-loader.js +68 -68
  8. package/src/index.js +146 -146
  9. package/src/index.mjs +123 -123
  10. package/src/server.js +463 -463
  11. package/src/services/apigateway/index.js +75 -75
  12. package/src/services/apigateway/server.js +607 -607
  13. package/src/services/apigateway/simulator.js +1405 -1405
  14. package/src/services/athena/index.js +75 -75
  15. package/src/services/athena/server.js +101 -101
  16. package/src/services/athena/simulador.js +998 -998
  17. package/src/services/athena/simulator.js +346 -346
  18. package/src/services/cloudformation/index.js +106 -106
  19. package/src/services/cloudformation/server.js +417 -417
  20. package/src/services/cloudformation/simulador.js +1020 -1020
  21. package/src/services/cloudtrail/index.js +84 -84
  22. package/src/services/cloudtrail/server.js +235 -235
  23. package/src/services/cloudtrail/simulador.js +719 -719
  24. package/src/services/cloudwatch/index.js +84 -84
  25. package/src/services/cloudwatch/server.js +366 -366
  26. package/src/services/cloudwatch/simulador.js +1173 -1173
  27. package/src/services/cognito/index.js +79 -79
  28. package/src/services/cognito/server.js +297 -297
  29. package/src/services/cognito/simulator.js +1992 -1761
  30. package/src/services/config/index.js +96 -96
  31. package/src/services/config/server.js +215 -215
  32. package/src/services/config/simulador.js +1260 -1260
  33. package/src/services/dynamodb/index.js +74 -74
  34. package/src/services/dynamodb/server.js +139 -139
  35. package/src/services/dynamodb/simulator.js +1005 -982
  36. package/src/services/dynamodb/sqlite-store.js +722 -0
  37. package/src/services/ecs/index.js +65 -65
  38. package/src/services/ecs/server.js +235 -235
  39. package/src/services/ecs/simulator.js +844 -844
  40. package/src/services/eventbridge/index.js +89 -89
  41. package/src/services/eventbridge/server.js +209 -209
  42. package/src/services/eventbridge/simulator.js +684 -684
  43. package/src/services/index.js +45 -45
  44. package/src/services/kms/index.js +75 -75
  45. package/src/services/kms/server.js +81 -81
  46. package/src/services/kms/simulator.js +344 -344
  47. package/src/services/lambda/handler-loader.js +183 -183
  48. package/src/services/lambda/index.js +81 -81
  49. package/src/services/lambda/route-registry.js +274 -274
  50. package/src/services/lambda/server.js +191 -191
  51. package/src/services/lambda/simulator.js +364 -364
  52. package/src/services/parameter-store/index.js +80 -80
  53. package/src/services/parameter-store/server.js +50 -50
  54. package/src/services/parameter-store/simulator.js +201 -201
  55. package/src/services/s3/index.js +73 -73
  56. package/src/services/s3/server.js +350 -350
  57. package/src/services/s3/simulator.js +568 -568
  58. package/src/services/secret-manager/index.js +80 -80
  59. package/src/services/secret-manager/server.js +51 -51
  60. package/src/services/secret-manager/simulator.js +182 -182
  61. package/src/services/sns/index.js +89 -89
  62. package/src/services/sns/server.js +607 -607
  63. package/src/services/sns/simulator.js +1482 -1482
  64. package/src/services/sqs/index.js +98 -98
  65. package/src/services/sqs/server.js +360 -360
  66. package/src/services/sqs/simulator.js +509 -509
  67. package/src/services/sts/index.js +37 -37
  68. package/src/services/sts/server.js +144 -144
  69. package/src/services/sts/simulator.js +69 -69
  70. package/src/services/xray/index.js +83 -83
  71. package/src/services/xray/server.js +308 -308
  72. package/src/services/xray/simulador.js +994 -994
  73. package/src/template/aws-config-template.js +87 -87
  74. package/src/template/aws-config-template.mjs +90 -90
  75. package/src/template/config-template.json +203 -203
  76. package/src/utils/aws-config.js +91 -91
  77. package/src/utils/cloudtrail-audit.js +129 -129
  78. package/src/utils/local-store.js +83 -83
  79. package/src/utils/logger.js +59 -59
@@ -1,982 +1,1005 @@
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
- const { CloudTrailAudit } = require("../../utils/cloudtrail-audit");
10
-
11
- class DynamoDBSimulator {
12
- constructor(config) {
13
- this.config = config;
14
- const dataDir = process.env.AWS_LOCAL_SIMULATOR_DATA_DIR || config.dataDir || "./.aws-local-simulator-data";
15
-
16
- if (!dataDir) {
17
- throw new Error("AWS_LOCAL_SIMULATOR_DATA_DIR not set");
18
- }
19
-
20
- this.dataDir = path.join(dataDir, "dynamodb");
21
- this.store = new LocalStore(this.dataDir);
22
- this.tables = new Map();
23
- this.audit = new CloudTrailAudit("dynamodb.amazonaws.com");
24
- // Mutex por tabela para evitar race condition em escritas concorrentes
25
- this._writeLocks = new Map();
26
- }
27
-
28
- /**
29
- * Executa fn com exclusão mútua por tableName.
30
- * Garante que escritas na mesma tabela não se sobreponham.
31
- */
32
- _withTableLock(tableName, fn) {
33
- const prev = this._writeLocks.get(tableName) || Promise.resolve();
34
- const next = prev.then(() => fn());
35
- // Guarda apenas a tail da cadeia (sem acumular referências)
36
- this._writeLocks.set(tableName, next.catch(() => {}));
37
- return next;
38
- }
39
- async initialize() {
40
- logger.debug("Inicializando DynamoDB Simulator...");
41
- this.loadTables();
42
- this._watchTablesFile();
43
- logger.debug(`✅ DynamoDB Simulator inicializado com ${this.tables.size} tabelas`);
44
- }
45
-
46
- _watchTablesFile() {
47
- const fs = require("fs");
48
- const tablesFilePath = this.store.getFilePath("__tables__");
49
- if (!fs.existsSync(tablesFilePath)) return;
50
-
51
- let reloadTimeout = null;
52
- fs.watch(tablesFilePath, (eventType) => {
53
- if (eventType !== "change") return;
54
- clearTimeout(reloadTimeout);
55
- reloadTimeout = setTimeout(() => {
56
- try {
57
- this.tables.clear();
58
- const savedTables = this.store.read("__tables__");
59
- if (savedTables) {
60
- for (const [name, definition] of Object.entries(savedTables)) {
61
- this.tables.set(name, definition);
62
- }
63
- }
64
- logger.info(`🔄 DynamoDB schema recarregado (${this.tables.size} tabelas)`);
65
- } catch (err) {
66
- logger.warn(`⚠️ Erro ao recarregar schema: ${err.message}`);
67
- }
68
- }, 200);
69
- });
70
-
71
- logger.debug(`👁️ Watching: ${tablesFilePath}`);
72
- }
73
-
74
- loadTables() {
75
- // Carrega tabelas existentes do disco PRIMEIRO para evitar sobrescrever definições persistidas
76
- const savedTables = this.store.read("__tables__");
77
- if (savedTables) {
78
- for (const [name, definition] of Object.entries(savedTables)) {
79
- this.tables.set(name, definition);
80
- }
81
- }
82
-
83
- // Cria tabelas da configuração ou atualiza schema (GSIs/attributeTypes) se já existirem
84
- if (this.config.dynamodb?.tables) {
85
- for (const tableDef of this.config.dynamodb.tables) {
86
- const { TableName, AttributeDefinitions, GlobalSecondaryIndexes } = tableDef;
87
- if (this.tables.has(TableName)) {
88
- // Tabela já existe no disco — atualiza schema sem apagar dados
89
- const existing = this.tables.get(TableName);
90
-
91
- if (AttributeDefinitions) {
92
- const attributeTypes = {};
93
- AttributeDefinitions.forEach((attr) => {
94
- attributeTypes[attr.AttributeName] = attr.AttributeType;
95
- });
96
- existing.attributeTypes = attributeTypes;
97
- }
98
-
99
- if (GlobalSecondaryIndexes) {
100
- const globalSecondaryIndexes = {};
101
- for (const gsi of GlobalSecondaryIndexes) {
102
- const gsiHashKey = gsi.KeySchema.find((k) => k.KeyType === "HASH").AttributeName;
103
- const gsiRangeKey = gsi.KeySchema.find((k) => k.KeyType === "RANGE")?.AttributeName;
104
- globalSecondaryIndexes[gsi.IndexName] = { hashKey: gsiHashKey, rangeKey: gsiRangeKey };
105
- }
106
- existing.globalSecondaryIndexes = globalSecondaryIndexes;
107
- }
108
-
109
- this.tables.set(TableName, existing);
110
- } else {
111
- this.createTable(tableDef);
112
- }
113
- }
114
- this.persistTables();
115
- }
116
- }
117
-
118
- createTable(params) {
119
- const { TableName, KeySchema, AttributeDefinitions, ProvisionedThroughput, GlobalSecondaryIndexes } = params;
120
-
121
- if (this.tables.has(TableName)) {
122
- logger.warn(`Tabela ${TableName} já existe`);
123
- return { TableDescription: { TableName, TableStatus: "ACTIVE" } };
124
- }
125
-
126
- const hashKey = KeySchema.find((k) => k.KeyType === "HASH").AttributeName;
127
- const rangeKey = KeySchema.find((k) => k.KeyType === "RANGE")?.AttributeName;
128
-
129
- const attributeTypes = {};
130
- AttributeDefinitions.forEach((attr) => {
131
- attributeTypes[attr.AttributeName] = attr.AttributeType;
132
- });
133
-
134
- const globalSecondaryIndexes = {};
135
- if (GlobalSecondaryIndexes) {
136
- for (const gsi of GlobalSecondaryIndexes) {
137
- const gsiHashKey = gsi.KeySchema.find((k) => k.KeyType === "HASH").AttributeName;
138
- const gsiRangeKey = gsi.KeySchema.find((k) => k.KeyType === "RANGE")?.AttributeName;
139
- globalSecondaryIndexes[gsi.IndexName] = { hashKey: gsiHashKey, rangeKey: gsiRangeKey };
140
- }
141
- }
142
-
143
- const table = {
144
- name: TableName,
145
- hashKey,
146
- rangeKey,
147
- attributeTypes,
148
- globalSecondaryIndexes,
149
- createdAt: new Date().toISOString(),
150
- itemCount: 0,
151
- sizeBytes: 0,
152
- };
153
-
154
- this.tables.set(TableName, table);
155
- this.persistTables();
156
-
157
- // Inicializa arquivo de dados apenas se não existir (preserva dados entre reinicializações)
158
- if (!this.store.exists(TableName)) {
159
- this.store.write(TableName, []);
160
- }
161
-
162
- logger.debug(`✅ Tabela criada: ${TableName}`);
163
- this.audit.record({ eventName: "CreateTable", readOnly: false, resources: [{ ARN: `arn:aws:dynamodb:local:000000000000:table/${TableName}`, type: "AWS::DynamoDB::Table" }], requestParameters: { tableName: TableName } });
164
-
165
- return {
166
- TableDescription: {
167
- TableName,
168
- TableStatus: "ACTIVE",
169
- CreationDateTime: new Date().toISOString(),
170
- KeySchema,
171
- AttributeDefinitions,
172
- ProvisionedThroughput: ProvisionedThroughput || {
173
- ReadCapacityUnits: 5,
174
- WriteCapacityUnits: 5,
175
- },
176
- ItemCount: 0,
177
- TableSizeBytes: 0,
178
- },
179
- };
180
- }
181
-
182
- async handleRequest(target, params) {
183
- const action = target.split(".")[1];
184
-
185
- logger.verboso(`DynamoDB Action: ${action}`, params);
186
-
187
- const readActions = new Set(["GetItem", "BatchGetItem", "Query", "Scan", "DescribeTable", "ListTables"]);
188
- const dataActions = new Set(["PutItem", "GetItem", "UpdateItem", "DeleteItem", "BatchWriteItem", "BatchGetItem", "Query", "Scan"]);
189
-
190
- const result = (() => {
191
- switch (action) {
192
- case "CreateTable": return this.createTable(params);
193
- case "DescribeTable": return this.describeTable(params.TableName);
194
- case "ListTables": return this.listTables(params);
195
- case "DeleteTable": return this.deleteTable(params);
196
- case "PutItem": return this._withTableLock(params.TableName, () => this.putItem(params));
197
- case "GetItem": return this.getItem(params);
198
- case "UpdateItem": return this._withTableLock(params.TableName, () => this.updateItem(params));
199
- case "DeleteItem": return this._withTableLock(params.TableName, () => this.deleteItem(params));
200
- case "BatchWriteItem": return this.batchWriteItem(params);
201
- case "BatchGetItem": return this.batchGetItem(params);
202
- case "Query": return this.query(params);
203
- case "Scan": return this.scan(params);
204
- default: throw new Error(`Unsupported action: ${action}`);
205
- }
206
- })();
207
-
208
- const tableName = params.TableName;
209
- if (tableName) {
210
- this.audit.record({
211
- eventName: action,
212
- readOnly: readActions.has(action),
213
- isDataEvent: dataActions.has(action),
214
- resources: [{ ARN: `arn:aws:dynamodb:local:000000000000:table/${tableName}`, type: "AWS::DynamoDB::Table" }],
215
- requestParameters: { tableName },
216
- });
217
- }
218
-
219
- return result;
220
- }
221
-
222
- describeTable(tableName) {
223
- const table = this.tables.get(tableName);
224
- if (!table) {
225
- throw new Error(`Table ${tableName} does not exist`);
226
- }
227
-
228
- const items = this.store.read(tableName);
229
-
230
- return {
231
- Table: {
232
- TableName: table.name,
233
- TableStatus: "ACTIVE",
234
- CreationDateTime: table.createdAt,
235
- KeySchema: [{ AttributeName: table.hashKey, KeyType: "HASH" }, ...(table.rangeKey ? [{ AttributeName: table.rangeKey, KeyType: "RANGE" }] : [])],
236
- AttributeDefinitions: Object.entries(table.attributeTypes).map(([name, type]) => ({
237
- AttributeName: name,
238
- AttributeType: type,
239
- })),
240
- ItemCount: items.length,
241
- TableSizeBytes: JSON.stringify(items).length,
242
- GlobalSecondaryIndexes: Object.entries(table.globalSecondaryIndexes || {}).map(([indexName, gsi]) => ({
243
- IndexName: indexName,
244
- IndexStatus: "ACTIVE",
245
- KeySchema: [
246
- { AttributeName: gsi.hashKey, KeyType: "HASH" },
247
- ...(gsi.rangeKey ? [{ AttributeName: gsi.rangeKey, KeyType: "RANGE" }] : []),
248
- ],
249
- Projection: { ProjectionType: "ALL" },
250
- ProvisionedThroughput: {
251
- ReadCapacityUnits: 5,
252
- WriteCapacityUnits: 5,
253
- },
254
- })),
255
- ProvisionedThroughput: {
256
- ReadCapacityUnits: 5,
257
- WriteCapacityUnits: 5,
258
- },
259
- },
260
- };
261
- }
262
-
263
- listTables(params = {}) {
264
- const tableNames = Array.from(this.tables.keys());
265
- const { Limit = 100, ExclusiveStartTableName } = params;
266
-
267
- let startIndex = 0;
268
- if (ExclusiveStartTableName) {
269
- const index = tableNames.indexOf(ExclusiveStartTableName);
270
- if (index !== -1) startIndex = index + 1;
271
- }
272
-
273
- const result = tableNames.slice(startIndex, startIndex + Limit);
274
-
275
- return {
276
- TableNames: result,
277
- LastEvaluatedTableName: result.length === Limit ? result[result.length - 1] : undefined,
278
- };
279
- }
280
-
281
- deleteTable(params) {
282
- const { TableName } = params;
283
-
284
- if (!this.tables.has(TableName)) {
285
- throw new Error(`Table ${TableName} does not exist`);
286
- }
287
-
288
- this.tables.delete(TableName);
289
- this.store.delete(TableName);
290
- this.persistTables();
291
-
292
- return { TableDescription: { TableName, TableStatus: "DELETING" } };
293
- }
294
-
295
- putItem(params) {
296
- const { TableName, Item, ReturnValues = "NONE" } = params;
297
- const table = this.tables.get(TableName);
298
-
299
- if (!table) {
300
- throw new Error(`Table ${TableName} does not exist`);
301
- }
302
-
303
- // Normaliza o item
304
- const normalizedItem = this.normalizeItem(Item, table);
305
- normalizedItem._createdAt = normalizedItem._createdAt || new Date().toISOString();
306
- normalizedItem._updatedAt = new Date().toISOString();
307
-
308
- // Carrega dados existentes
309
- let items = this.store.read(TableName);
310
- const itemKey = this.getItemKey(normalizedItem, table);
311
-
312
- // Encontra e substitui ou adiciona
313
- const existingIndex = items.findIndex((item) => this.getItemKey(item, table) === itemKey);
314
-
315
- let oldItem = null;
316
- if (existingIndex !== -1) {
317
- oldItem = { ...items[existingIndex] };
318
- items[existingIndex] = normalizedItem;
319
- } else {
320
- items.push(normalizedItem);
321
- table.itemCount++;
322
- }
323
-
324
- // Salva no store
325
- this.store.write(TableName, items);
326
- this.persistTables();
327
-
328
- logger.verboso(`PutItem: ${TableName}/${itemKey}`);
329
-
330
- const response = {};
331
- if (ReturnValues === "ALL_OLD" && oldItem) {
332
- response.Attributes = this.marshallItem(oldItem, table);
333
- }
334
-
335
- return response;
336
- }
337
-
338
- getItem(params) {
339
- const { TableName, Key } = params;
340
- const table = this.tables.get(TableName);
341
-
342
- if (!table) {
343
- throw new Error(`Table ${TableName} does not exist`);
344
- }
345
-
346
- const items = this.store.read(TableName);
347
- const itemKey = this.getItemKeyFromKeys(Key, table);
348
-
349
- const item = items.find((item) => this.getItemKey(item, table) === itemKey);
350
-
351
- logger.verboso(`GetItem: ${TableName}/${itemKey} - ${item ? "found" : "not found"}`);
352
-
353
- return item ? { Item: this.marshallItem(item, table) } : {};
354
- }
355
-
356
- updateItem(params) {
357
- const { TableName, Key, UpdateExpression, ExpressionAttributeNames = {}, ExpressionAttributeValues = {}, ReturnValues = "NONE" } = params;
358
- const table = this.tables.get(TableName);
359
-
360
- if (!table) {
361
- throw new Error(`Table ${TableName} does not exist`);
362
- }
363
-
364
- // Busca o item atual (upsert: cria se não existir, como o DynamoDB real)
365
- const items = this.store.read(TableName);
366
- const itemKey = this.getItemKeyFromKeys(Key, table);
367
- const index = items.findIndex((item) => this.getItemKey(item, table) === itemKey);
368
-
369
- // Se não existe, cria um novo item com as chaves fornecidas
370
- if (index === -1) {
371
- const newItem = this.normalizeItem(Key, table);
372
- newItem._createdAt = new Date().toISOString();
373
- newItem._updatedAt = new Date().toISOString();
374
- if (UpdateExpression) {
375
- this.processUpdateExpression(newItem, UpdateExpression, ExpressionAttributeNames, ExpressionAttributeValues, table);
376
- }
377
- items.push(newItem);
378
- this.store.write(TableName, items);
379
- logger.verboso(`UpdateItem (upsert): ${TableName}/${itemKey}`);
380
- const response = {};
381
- if (ReturnValues === "ALL_NEW" || ReturnValues === "UPDATED_NEW") {
382
- response.Attributes = this.marshallItem(newItem, table);
383
- }
384
- return response;
385
- }
386
-
387
- const currentItem = items[index];
388
- const updatedItem = { ...currentItem };
389
- updatedItem._updatedAt = new Date().toISOString();
390
-
391
- // Processa a UpdateExpression
392
- if (UpdateExpression) {
393
- this.processUpdateExpression(updatedItem, UpdateExpression, ExpressionAttributeNames, ExpressionAttributeValues, table);
394
- }
395
-
396
- // Salva o item atualizado
397
- const oldItem = { ...items[index] };
398
- items[index] = updatedItem;
399
- this.store.write(TableName, items);
400
-
401
- logger.verboso(`UpdateItem: ${TableName}/${itemKey}`);
402
-
403
- const response = {};
404
- switch (ReturnValues) {
405
- case "ALL_OLD":
406
- response.Attributes = this.marshallItem(oldItem, table);
407
- break;
408
- case "ALL_NEW":
409
- case "UPDATED_NEW":
410
- response.Attributes = this.marshallItem(updatedItem, table);
411
- break;
412
- case "UPDATED_OLD":
413
- response.Attributes = this.marshallItem(oldItem, table);
414
- break;
415
- default:
416
- break;
417
- }
418
-
419
- return response;
420
- }
421
-
422
- deleteItem(params) {
423
- const { TableName, Key, ReturnValues = "NONE" } = params;
424
- const table = this.tables.get(TableName);
425
-
426
- if (!table) {
427
- throw new Error(`Table ${TableName} does not exist`);
428
- }
429
-
430
- const items = this.store.read(TableName);
431
- const itemKey = this.getItemKeyFromKeys(Key, table);
432
- const index = items.findIndex((item) => this.getItemKey(item, table) === itemKey);
433
-
434
- if (index === -1) {
435
- return {};
436
- }
437
-
438
- const oldItem = { ...items[index] };
439
- items.splice(index, 1);
440
- this.store.write(TableName, items);
441
- table.itemCount--;
442
- this.persistTables();
443
-
444
- logger.verboso(`DeleteItem: ${TableName}/${itemKey}`);
445
-
446
- const response = {};
447
- if (ReturnValues === "ALL_OLD") {
448
- response.Attributes = this.marshallItem(oldItem, table);
449
- }
450
-
451
- return response;
452
- }
453
-
454
- batchWriteItem(params) {
455
- const { RequestItems } = params;
456
- const responses = {};
457
-
458
- if (!RequestItems) {
459
- logger.debug(`[DEBUG batchWriteItem] params recebido: ${JSON.stringify(params)}`);
460
- throw new Error(`RequestItems is required for BatchWriteItem. Params received: ${JSON.stringify(params)}`);
461
- }
462
-
463
- // Serializa por tabela usando o mutex para evitar race condition
464
- const tablePromises = Object.entries(RequestItems).map(([tableName, operations]) =>
465
- this._withTableLock(tableName, () => {
466
- const table = this.tables.get(tableName);
467
- if (!table) {
468
- logger.info(`[BATCH-DEBUG] tabela não encontrada: ${tableName}`);
469
- return;
470
- }
471
-
472
- const itemsBefore = this.store.read(tableName);
473
- logger.info(`[BATCH-DEBUG] ${tableName} | antes=${itemsBefore.length} | ops=${operations.length}`);
474
-
475
- let items = [...itemsBefore];
476
- const unprocessedItems = [];
477
- let inserts = 0;
478
- let updates = 0;
479
-
480
- for (const op of operations) {
481
- if (op.PutRequest) {
482
- const item = this.normalizeItem(op.PutRequest.Item, table);
483
- const itemKey = this.getItemKey(item, table);
484
- const index = items.findIndex((i) => this.getItemKey(i, table) === itemKey);
485
-
486
- if (index !== -1) {
487
- items[index] = item;
488
- updates++;
489
- } else {
490
- items.push(item);
491
- table.itemCount++;
492
- inserts++;
493
- }
494
- } else if (op.DeleteRequest) {
495
- const key = op.DeleteRequest.Key;
496
- const itemKey = this.getItemKeyFromKeys(key, table);
497
- const index = items.findIndex((i) => this.getItemKey(i, table) === itemKey);
498
-
499
- if (index !== -1) {
500
- items.splice(index, 1);
501
- table.itemCount--;
502
- } else {
503
- unprocessedItems.push(op);
504
- }
505
- }
506
- }
507
-
508
- this.store.write(tableName, items);
509
- const itemsAfter = this.store.read(tableName);
510
- logger.info(`[BATCH-DEBUG] ${tableName} | depois=${itemsAfter.length} | esperado=${items.length} | match=${itemsAfter.length === items.length} | inserts=${inserts} | updates=${updates}`);
511
-
512
- if (unprocessedItems.length > 0) {
513
- responses[tableName] = unprocessedItems;
514
- }
515
- })
516
- );
517
-
518
- return Promise.all(tablePromises).then(() => {
519
- this.persistTables();
520
- const finalCount = this.store.read(Object.keys(RequestItems)[0]).length;
521
- //logger.info(`[BATCH-DEBUG] FINAL | tabela=${Object.keys(RequestItems)[0]} | total=${finalCount}`);
522
- return { UnprocessedItems: responses };
523
- });
524
- }
525
-
526
- batchGetItem(params) {
527
- const { RequestItems } = params;
528
- const responses = {};
529
-
530
- for (const [tableName, request] of Object.entries(RequestItems)) {
531
- const table = this.tables.get(tableName);
532
- if (!table) continue;
533
-
534
- const items = this.store.read(tableName);
535
- const { Keys } = request;
536
- const foundItems = [];
537
-
538
- for (const key of Keys) {
539
- const itemKey = this.getItemKeyFromKeys(key, table);
540
- const item = items.find((i) => this.getItemKey(i, table) === itemKey);
541
- if (item) {
542
- foundItems.push(this.marshallItem(item, table));
543
- }
544
- }
545
-
546
- responses[tableName] = { Items: foundItems };
547
- }
548
-
549
- return { Responses: responses };
550
- }
551
-
552
- query(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;
565
- const table = this.tables.get(TableName);
566
-
567
- if (!table) {
568
- throw new Error(`Table ${TableName} does not exist`);
569
- }
570
-
571
- let items = this.store.read(TableName);
572
-
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);
579
- }
580
- }
581
-
582
- // Helper para resolver nomes de atributos (que podem ser placeholders como #n0)
583
- const resolveAttributeName = (name) => {
584
- if (name.startsWith("#")) return ExpressionAttributeNames[name] || name;
585
- return name;
586
- };
587
-
588
- // Helper para extrair valor de placeholder (ex: :v0)
589
- const resolveValue = (placeholder) => {
590
- const rawValue = ExpressionAttributeValues[placeholder];
591
- if (rawValue === undefined) return undefined;
592
- if (rawValue !== null && typeof rawValue === 'object' && !Array.isArray(rawValue)) {
593
- const keys = Object.keys(rawValue);
594
- if (keys.length === 1 && ["S", "N", "BOOL", "NULL", "M", "L", "SS", "NS", "BS"].includes(keys[0])) {
595
- return this.normalizeValue(rawValue, table);
596
- }
597
- }
598
- return rawValue;
599
- };
600
-
601
- // Filtra pela KeyConditionExpression
602
- if (KeyConditionExpression) {
603
- const parts = KeyConditionExpression.split(/\s+AND\s+/i);
604
- for (const part of parts) {
605
- const match = part.match(/([^\s]+)\s*(=|>|<|>=|<=|BEGINS_WITH|BETWEEN)\s*([^\s]+)(?:\s+AND\s+([^\s]+))?/i);
606
- if (match) {
607
- const attrPlaceholder = match[1];
608
- const operator = match[2].toUpperCase();
609
- const valPlaceholder = match[3];
610
- const attributeName = resolveAttributeName(attrPlaceholder);
611
- const expectedValue = resolveValue(valPlaceholder);
612
-
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") {
619
- const val = expectedValue;
620
- items = items.filter(item => String(item[attributeName] || "").startsWith(String(val)));
621
- }
622
- }
623
- }
624
- }
625
-
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
- }
684
-
685
- return {
686
- Items: marshalledItems,
687
- Count: marshalledItems.length,
688
- ScannedCount: scannedCount,
689
- LastEvaluatedKey: lastEvaluatedKey || undefined
690
- };
691
- }
692
-
693
- scan(params) {
694
- const {
695
- TableName,
696
- FilterExpression,
697
- ExpressionAttributeValues,
698
- ExpressionAttributeNames = {},
699
- Limit,
700
- ExclusiveStartKey,
701
- ProjectionExpression
702
- } = params;
703
- const table = this.tables.get(TableName);
704
-
705
- if (!table) {
706
- throw new Error(`Table ${TableName} does not exist`);
707
- }
708
-
709
- const allItems = this.store.read(TableName);
710
- let items = allItems;
711
- const scannedCount = items.length;
712
-
713
- // Aplica filtro se existir
714
- if (FilterExpression) {
715
- items = this.applyFilter(items, FilterExpression, ExpressionAttributeValues, ExpressionAttributeNames, table);
716
- }
717
-
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;
729
- if (Limit && items.length > Limit) {
730
- const lastItem = items[Limit - 1];
731
- lastEvaluatedKey = this.marshallItem(lastItem, table);
732
- items = items.slice(0, Limit);
733
- }
734
-
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
- }
741
-
742
- return {
743
- Items: marshalledItems,
744
- Count: marshalledItems.length,
745
- ScannedCount: scannedCount,
746
- LastEvaluatedKey: lastEvaluatedKey || undefined
747
- };
748
- }
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
-
768
-
769
- // Métodos auxiliares
770
- normalizeItem(item, table) {
771
- const normalized = { ...item };
772
-
773
- for (const [key, value] of Object.entries(normalized)) {
774
- normalized[key] = this.normalizeValue(value, table);
775
- }
776
-
777
- return normalized;
778
- }
779
-
780
- normalizeValue(value, table) {
781
- if (value === null || value === undefined) return value;
782
- if (typeof value !== 'object') return value;
783
-
784
- if (value.S !== undefined) return value.S;
785
- if (value.N !== undefined) return parseFloat(value.N);
786
- if (value.BOOL !== undefined) return value.BOOL;
787
- if (value.NULL !== undefined) return null;
788
- if (value.L !== undefined) return value.L.map((v) => this.normalizeValue(v, table));
789
- if (value.M !== undefined) return this.normalizeItem(value.M, table);
790
- if (value.SS !== undefined) return value.SS;
791
- if (value.NS !== undefined) return value.NS.map(Number);
792
-
793
- // plain object (already normalized, e.g. stored without DynamoDB types)
794
- return value;
795
- }
796
-
797
- marshallValue(value, table) {
798
- if (value === null || value === undefined) return { NULL: true };
799
- if (typeof value === 'boolean') return { BOOL: value };
800
- if (typeof value === 'number') return { N: String(value) };
801
- if (typeof value === 'string') return { S: value };
802
- if (Array.isArray(value)) return { L: value.map((v) => this.marshallValue(v, table)) };
803
- if (typeof value === 'object') return { M: this.marshallItem(value, table) };
804
- return { S: String(value) };
805
- }
806
-
807
- marshallItem(item, table) {
808
- const marshalled = {};
809
-
810
- for (const [key, value] of Object.entries(item)) {
811
- if (key.startsWith("_")) continue; // Pula campos internos
812
-
813
- const type = table ? table.attributeTypes[key] : null;
814
- if (type === "S") {
815
- marshalled[key] = { S: String(value) };
816
- } else if (type === "N") {
817
- marshalled[key] = { N: String(value) };
818
- } else {
819
- marshalled[key] = this.marshallValue(value, table);
820
- }
821
- }
822
-
823
- return marshalled;
824
- }
825
-
826
- getItemKey(item, table) {
827
- const hashValue = item[table.hashKey];
828
- const rangeValue = table.rangeKey ? item[table.rangeKey] : null;
829
- return rangeValue ? `${hashValue}|${rangeValue}` : String(hashValue);
830
- }
831
-
832
- getItemKeyFromKeys(keys, table) {
833
- const rawHash = keys[table.hashKey];
834
- const hashValue = rawHash && typeof rawHash === 'object' ? Object.values(rawHash)[0] : rawHash;
835
- const rawRange = table.rangeKey ? keys[table.rangeKey] : null;
836
- const rangeValue = rawRange && typeof rawRange === 'object' ? Object.values(rawRange)[0] : rawRange;
837
- return rangeValue ? `${hashValue}|${rangeValue}` : String(hashValue);
838
- }
839
-
840
- processUpdateExpression(item, expression, nameMap, valueMap, table) {
841
- // SET clause
842
- const setMatch = expression.match(/SET\s+([^]+?)(?=\s+(?:REMOVE|ADD|DELETE)\s|\s*$)/i);
843
- if (setMatch) {
844
- const assignments = setMatch[1].split(",").map((a) => a.trim());
845
- for (const assignment of assignments) {
846
- const [path, valueExpr] = assignment.split("=").map((s) => s.trim());
847
- const attributeName = nameMap[path] || path.replace(/#/g, "");
848
- const rawValue = valueMap[valueExpr];
849
- // Usa normalizeValue para garantir o mesmo formato que o putItem
850
- item[attributeName] = this.normalizeValue(rawValue, table);
851
- }
852
- }
853
-
854
- // ADD clause — incrementa números ou adiciona a sets (upsert-friendly)
855
- const addMatch = expression.match(/ADD\s+([^]+?)(?=\s+(?:SET|REMOVE|DELETE)\s|\s*$)/i);
856
- if (addMatch) {
857
- const assignments = addMatch[1].split(",").map((a) => a.trim());
858
- for (const assignment of assignments) {
859
- const parts = assignment.split(/\s+/);
860
- const attributeName = nameMap[parts[0]] || parts[0].replace(/#/g, "");
861
- const rawValue = valueMap[parts[1]];
862
- // Usa normalizeValue para garantir o mesmo formato que o putItem
863
- const delta = this.normalizeValue(rawValue, table);
864
- const current = item[attributeName];
865
- if (current === undefined || current === null) {
866
- item[attributeName] = typeof delta === 'number' ? delta : parseFloat(delta) || 0;
867
- } else {
868
- item[attributeName] = (parseFloat(current) || 0) + (parseFloat(delta) || 0);
869
- }
870
- }
871
- }
872
-
873
- // REMOVE clause
874
- const removeMatch = expression.match(/REMOVE\s+([^]+?)(?=\s+(?:SET|ADD|DELETE)\s|\s*$)/i);
875
- if (removeMatch) {
876
- const attributes = removeMatch[1].split(",").map((a) => a.trim());
877
- for (const attr of attributes) {
878
- const attributeName = nameMap[attr] || attr.replace(/#/g, "");
879
- delete item[attributeName];
880
- }
881
- }
882
- }
883
-
884
- applyFilter(items, expression, values, names, table) {
885
- if (!expression) return items;
886
-
887
- const resolveAttributeName = (name) => {
888
- if (name.startsWith("#")) return names[name] || name;
889
- return name;
890
- };
891
-
892
- const resolveValue = (placeholder) => {
893
- const rawValue = values[placeholder];
894
- if (rawValue === undefined) return undefined;
895
- if (rawValue !== null && typeof rawValue === 'object' && !Array.isArray(rawValue)) {
896
- const keys = Object.keys(rawValue);
897
- if (keys.length === 1 && ["S", "N", "BOOL", "NULL", "M", "L", "SS", "NS", "BS"].includes(keys[0])) {
898
- return this.normalizeValue(rawValue, table);
899
- }
900
- }
901
- return rawValue;
902
- };
903
-
904
- const conditions = expression.split(/\s+AND\s+/i);
905
-
906
- return items.filter((item) => {
907
- return conditions.every(cond => {
908
- // Regex para match de funções como contains(#n, :v) ou begins_with(#n, :v)
909
- const funcMatch = cond.match(/(contains|begins_with)\s*\(\s*([^\s,]+)\s*,\s*([^\s,)]+)\s*\)/i);
910
- if (funcMatch) {
911
- const func = funcMatch[1].toLowerCase();
912
- const attrName = resolveAttributeName(funcMatch[2]);
913
- const expectedVal = resolveValue(funcMatch[3]);
914
- const actualVal = item[attrName];
915
-
916
- if (func === 'contains') {
917
- if (Array.isArray(actualVal)) return actualVal.includes(expectedVal);
918
- return String(actualVal || "").includes(String(expectedVal));
919
- }
920
- if (func === 'begins_with') {
921
- return String(actualVal || "").startsWith(String(expectedVal));
922
- }
923
- }
924
-
925
- // Regex para operadores básicos
926
- const opMatch = cond.match(/([^\s]+)\s*(=|<>|<|<=|>|>=)\s*([^\s]+)/);
927
- if (opMatch) {
928
- const attrName = resolveAttributeName(opMatch[1]);
929
- const operator = opMatch[2];
930
- const expectedVal = resolveValue(opMatch[3]);
931
- const actualVal = item[attrName];
932
-
933
- switch (operator) {
934
- case "=": return actualVal === expectedVal;
935
- case "<>": return actualVal !== expectedVal;
936
- case "<": return actualVal < expectedVal;
937
- case "<=": return actualVal <= expectedVal;
938
- case ">": return actualVal > expectedVal;
939
- case ">=": return actualVal >= expectedVal;
940
- default: return true;
941
- }
942
- }
943
- return true;
944
- });
945
- });
946
- }
947
-
948
-
949
- persistTables() {
950
- const tablesObj = {};
951
- for (const [name, table] of this.tables.entries()) {
952
- tablesObj[name] = table;
953
- }
954
- this.store.write("__tables__", tablesObj);
955
- }
956
-
957
- async reset() {
958
- for (const [tableName] of this.tables) {
959
- this.store.write(tableName, []);
960
- }
961
- logger.debug("DynamoDB: Todos os dados resetados");
962
- }
963
-
964
- getTablesCount() {
965
- return this.tables.size;
966
- }
967
-
968
- getTotalItems() {
969
- let total = 0;
970
- for (const [tableName] of this.tables) {
971
- const items = this.store.read(tableName);
972
- total += items.length;
973
- }
974
- return total;
975
- }
976
-
977
- listTables() {
978
- return { TableNames: Array.from(this.tables.keys()) };
979
- }
980
- }
981
-
982
- module.exports = DynamoDBSimulator;
1
+ /**
2
+ * DynamoDB Simulator Core
3
+ */
4
+ const SQLiteStore = require('./sqlite-store');
5
+ const LocalStore = require("../../utils/local-store");
6
+ const logger = require("../../utils/logger");
7
+ const crypto = require("crypto");
8
+ const path = require("path");
9
+ const { CloudTrailAudit } = require("../../utils/cloudtrail-audit");
10
+
11
+ class DynamoDBSimulator {
12
+ constructor(config) {
13
+ this.config = config;
14
+ const dataDir = process.env.AWS_LOCAL_SIMULATOR_DATA_DIR || config.dataDir || "./.aws-local-simulator-data";
15
+ this.useSQLite = process.env.DYNAMODB_USE_SQLITE == 'true'; // Default false
16
+
17
+ if (!dataDir) {
18
+ throw new Error("AWS_LOCAL_SIMULATOR_DATA_DIR not set");
19
+ }
20
+
21
+ this.dataDir = path.join(dataDir, "dynamodb");
22
+
23
+ // Escolhe o store baseado na configuração
24
+ if (this.useSQLite) {
25
+ this.store = new SQLiteStore(this.dataDir);
26
+ logger.info("📦 Usando SQLite Store para persistência");
27
+ } else {
28
+ this.store = new LocalStore(this.dataDir);
29
+ logger.info("📄 Usando JSON Store (legacy) para persistência");
30
+ }
31
+
32
+
33
+ this.tables = new Map();
34
+ this.audit = new CloudTrailAudit("dynamodb.amazonaws.com");
35
+ // Mutex por tabela para evitar race condition em escritas concorrentes
36
+ this._writeLocks = new Map();
37
+ }
38
+
39
+ /**
40
+ * Executa fn com exclusão mútua por tableName.
41
+ * Garante que escritas na mesma tabela não se sobreponham.
42
+ */
43
+ _withTableLock(tableName, fn) {
44
+ const prev = this._writeLocks.get(tableName) || Promise.resolve();
45
+ const next = prev.then(() => fn());
46
+ // Guarda apenas a tail da cadeia (sem acumular referências)
47
+ this._writeLocks.set(tableName, next.catch(() => { }));
48
+ return next;
49
+ }
50
+ async initialize() {
51
+ logger.debug("Inicializando DynamoDB Simulator...");
52
+ this.loadTables();
53
+ this._watchTablesFile();
54
+ logger.debug(`✅ DynamoDB Simulator inicializado com ${this.tables.size} tabelas`);
55
+ }
56
+
57
+ _watchTablesFile() {
58
+ const fs = require("fs");
59
+ const tablesFilePath = this.store.getFilePath("__tables__");
60
+ if (!fs.existsSync(tablesFilePath)) return;
61
+
62
+ let reloadTimeout = null;
63
+ fs.watch(tablesFilePath, (eventType) => {
64
+ if (eventType !== "change") return;
65
+ clearTimeout(reloadTimeout);
66
+ reloadTimeout = setTimeout(() => {
67
+ try {
68
+ this.tables.clear();
69
+ const savedTables = this.store.read("__tables__");
70
+ if (savedTables) {
71
+ for (const [name, definition] of Object.entries(savedTables)) {
72
+ this.tables.set(name, definition);
73
+ }
74
+ }
75
+ logger.info(`🔄 DynamoDB schema recarregado (${this.tables.size} tabelas)`);
76
+ } catch (err) {
77
+ logger.warn(`⚠️ Erro ao recarregar schema: ${err.message}`);
78
+ }
79
+ }, 200);
80
+ });
81
+
82
+ logger.debug(`👁️ Watching: ${tablesFilePath}`);
83
+ }
84
+
85
+ loadTables() {
86
+ // Carrega tabelas existentes do disco PRIMEIRO para evitar sobrescrever definições persistidas
87
+ const savedTables = this.store.read("__tables__");
88
+ if (savedTables) {
89
+ for (const [name, definition] of Object.entries(savedTables)) {
90
+ this.tables.set(name, definition);
91
+ }
92
+ }
93
+
94
+ // Cria tabelas da configuração ou atualiza schema (GSIs/attributeTypes) se já existirem
95
+ if (this.config.dynamodb?.tables) {
96
+ for (const tableDef of this.config.dynamodb.tables) {
97
+ const { TableName, AttributeDefinitions, GlobalSecondaryIndexes } = tableDef;
98
+ if (this.tables.has(TableName)) {
99
+ // Tabela já existe no disco — atualiza schema sem apagar dados
100
+ const existing = this.tables.get(TableName);
101
+
102
+ if (AttributeDefinitions) {
103
+ const attributeTypes = {};
104
+ AttributeDefinitions.forEach((attr) => {
105
+ attributeTypes[attr.AttributeName] = attr.AttributeType;
106
+ });
107
+ existing.attributeTypes = attributeTypes;
108
+ }
109
+
110
+ if (GlobalSecondaryIndexes) {
111
+ const globalSecondaryIndexes = {};
112
+ for (const gsi of GlobalSecondaryIndexes) {
113
+ const gsiHashKey = gsi.KeySchema.find((k) => k.KeyType === "HASH").AttributeName;
114
+ const gsiRangeKey = gsi.KeySchema.find((k) => k.KeyType === "RANGE")?.AttributeName;
115
+ globalSecondaryIndexes[gsi.IndexName] = { hashKey: gsiHashKey, rangeKey: gsiRangeKey };
116
+ }
117
+ existing.globalSecondaryIndexes = globalSecondaryIndexes;
118
+ }
119
+
120
+ this.tables.set(TableName, existing);
121
+ } else {
122
+ this.createTable(tableDef);
123
+ }
124
+ }
125
+ this.persistTables();
126
+ }
127
+ }
128
+
129
+ createTable(params) {
130
+ const { TableName, KeySchema, AttributeDefinitions, ProvisionedThroughput, GlobalSecondaryIndexes } = params;
131
+
132
+ if (this.tables.has(TableName)) {
133
+ logger.warn(`Tabela ${TableName} já existe`);
134
+ return { TableDescription: { TableName, TableStatus: "ACTIVE" } };
135
+ }
136
+
137
+ const hashKey = KeySchema.find((k) => k.KeyType === "HASH").AttributeName;
138
+ const rangeKey = KeySchema.find((k) => k.KeyType === "RANGE")?.AttributeName;
139
+
140
+ const attributeTypes = {};
141
+ AttributeDefinitions.forEach((attr) => {
142
+ attributeTypes[attr.AttributeName] = attr.AttributeType;
143
+ });
144
+
145
+ const globalSecondaryIndexes = {};
146
+ if (GlobalSecondaryIndexes) {
147
+ for (const gsi of GlobalSecondaryIndexes) {
148
+ const gsiHashKey = gsi.KeySchema.find((k) => k.KeyType === "HASH").AttributeName;
149
+ const gsiRangeKey = gsi.KeySchema.find((k) => k.KeyType === "RANGE")?.AttributeName;
150
+ globalSecondaryIndexes[gsi.IndexName] = { hashKey: gsiHashKey, rangeKey: gsiRangeKey };
151
+ }
152
+ }
153
+
154
+ const table = {
155
+ name: TableName,
156
+ hashKey,
157
+ rangeKey,
158
+ attributeTypes,
159
+ globalSecondaryIndexes,
160
+ createdAt: new Date().toISOString(),
161
+ itemCount: 0,
162
+ sizeBytes: 0,
163
+ };
164
+
165
+ this.tables.set(TableName, table);
166
+ this.persistTables();
167
+
168
+ // Inicializa arquivo de dados apenas se não existir (preserva dados entre reinicializações)
169
+ if (!this.store.exists(TableName)) {
170
+ this.store.write(TableName, []);
171
+ }
172
+
173
+ logger.debug(`✅ Tabela criada: ${TableName}`);
174
+ this.audit.record({ eventName: "CreateTable", readOnly: false, resources: [{ ARN: `arn:aws:dynamodb:local:000000000000:table/${TableName}`, type: "AWS::DynamoDB::Table" }], requestParameters: { tableName: TableName } });
175
+
176
+ return {
177
+ TableDescription: {
178
+ TableName,
179
+ TableStatus: "ACTIVE",
180
+ CreationDateTime: new Date().toISOString(),
181
+ KeySchema,
182
+ AttributeDefinitions,
183
+ ProvisionedThroughput: ProvisionedThroughput || {
184
+ ReadCapacityUnits: 5,
185
+ WriteCapacityUnits: 5,
186
+ },
187
+ ItemCount: 0,
188
+ TableSizeBytes: 0,
189
+ },
190
+ };
191
+ }
192
+
193
+ async handleRequest(target, params) {
194
+ const action = target.split(".")[1];
195
+
196
+ logger.verboso(`DynamoDB Action: ${action}`, params);
197
+
198
+ const readActions = new Set(["GetItem", "BatchGetItem", "Query", "Scan", "DescribeTable", "ListTables"]);
199
+ const dataActions = new Set(["PutItem", "GetItem", "UpdateItem", "DeleteItem", "BatchWriteItem", "BatchGetItem", "Query", "Scan"]);
200
+
201
+ const result = (() => {
202
+ switch (action) {
203
+ case "CreateTable": return this.createTable(params);
204
+ case "DescribeTable": return this.describeTable(params.TableName);
205
+ case "ListTables": return this.listTables(params);
206
+ case "DeleteTable": return this.deleteTable(params);
207
+ case "PutItem": return this._withTableLock(params.TableName, () => this.putItem(params));
208
+ case "GetItem": return this.getItem(params);
209
+ case "UpdateItem": return this._withTableLock(params.TableName, () => this.updateItem(params));
210
+ case "DeleteItem": return this._withTableLock(params.TableName, () => this.deleteItem(params));
211
+ case "BatchWriteItem": return this.batchWriteItem(params);
212
+ case "BatchGetItem": return this.batchGetItem(params);
213
+ case "Query": return this.query(params);
214
+ case "Scan": return this.scan(params);
215
+ default: throw new Error(`Unsupported action: ${action}`);
216
+ }
217
+ })();
218
+
219
+ const tableName = params.TableName;
220
+ if (tableName) {
221
+ this.audit.record({
222
+ eventName: action,
223
+ readOnly: readActions.has(action),
224
+ isDataEvent: dataActions.has(action),
225
+ resources: [{ ARN: `arn:aws:dynamodb:local:000000000000:table/${tableName}`, type: "AWS::DynamoDB::Table" }],
226
+ requestParameters: { tableName },
227
+ });
228
+ }
229
+
230
+ return result;
231
+ }
232
+
233
+ describeTable(tableName) {
234
+ const table = this.tables.get(tableName);
235
+ if (!table) {
236
+ throw new Error(`Table ${tableName} does not exist`);
237
+ }
238
+
239
+ const items = this.store.read(tableName);
240
+
241
+ return {
242
+ Table: {
243
+ TableName: table.name,
244
+ TableStatus: "ACTIVE",
245
+ CreationDateTime: table.createdAt,
246
+ KeySchema: [{ AttributeName: table.hashKey, KeyType: "HASH" }, ...(table.rangeKey ? [{ AttributeName: table.rangeKey, KeyType: "RANGE" }] : [])],
247
+ AttributeDefinitions: Object.entries(table.attributeTypes).map(([name, type]) => ({
248
+ AttributeName: name,
249
+ AttributeType: type,
250
+ })),
251
+ ItemCount: items.length,
252
+ TableSizeBytes: JSON.stringify(items).length,
253
+ GlobalSecondaryIndexes: Object.entries(table.globalSecondaryIndexes || {}).map(([indexName, gsi]) => ({
254
+ IndexName: indexName,
255
+ IndexStatus: "ACTIVE",
256
+ KeySchema: [
257
+ { AttributeName: gsi.hashKey, KeyType: "HASH" },
258
+ ...(gsi.rangeKey ? [{ AttributeName: gsi.rangeKey, KeyType: "RANGE" }] : []),
259
+ ],
260
+ Projection: { ProjectionType: "ALL" },
261
+ ProvisionedThroughput: {
262
+ ReadCapacityUnits: 5,
263
+ WriteCapacityUnits: 5,
264
+ },
265
+ })),
266
+ ProvisionedThroughput: {
267
+ ReadCapacityUnits: 5,
268
+ WriteCapacityUnits: 5,
269
+ },
270
+ },
271
+ };
272
+ }
273
+
274
+ listTables(params = {}) {
275
+ const tableNames = Array.from(this.tables.keys());
276
+ const { Limit = 100, ExclusiveStartTableName } = params;
277
+
278
+ let startIndex = 0;
279
+ if (ExclusiveStartTableName) {
280
+ const index = tableNames.indexOf(ExclusiveStartTableName);
281
+ if (index !== -1) startIndex = index + 1;
282
+ }
283
+
284
+ const result = tableNames.slice(startIndex, startIndex + Limit);
285
+
286
+ return {
287
+ TableNames: result,
288
+ LastEvaluatedTableName: result.length === Limit ? result[result.length - 1] : undefined,
289
+ };
290
+ }
291
+
292
+ deleteTable(params) {
293
+ const { TableName } = params;
294
+
295
+ if (!this.tables.has(TableName)) {
296
+ throw new Error(`Table ${TableName} does not exist`);
297
+ }
298
+
299
+ this.tables.delete(TableName);
300
+ this.store.delete(TableName);
301
+ this.persistTables();
302
+
303
+ return { TableDescription: { TableName, TableStatus: "DELETING" } };
304
+ }
305
+
306
+ putItem(params) {
307
+ const { TableName, Item, ReturnValues = "NONE" } = params;
308
+ const table = this.tables.get(TableName);
309
+
310
+ if (!table) {
311
+ throw new Error(`Table ${TableName} does not exist`);
312
+ }
313
+
314
+ // Normaliza o item
315
+ const normalizedItem = this.normalizeItem(Item, table);
316
+ normalizedItem._createdAt = normalizedItem._createdAt || new Date().toISOString();
317
+ normalizedItem._updatedAt = new Date().toISOString();
318
+
319
+ // Carrega dados existentes
320
+ let items = this.store.read(TableName);
321
+ const itemKey = this.getItemKey(normalizedItem, table);
322
+
323
+ // Encontra e substitui ou adiciona
324
+ const existingIndex = items.findIndex((item) => this.getItemKey(item, table) === itemKey);
325
+
326
+ let oldItem = null;
327
+ if (existingIndex !== -1) {
328
+ oldItem = { ...items[existingIndex] };
329
+ items[existingIndex] = normalizedItem;
330
+ } else {
331
+ items.push(normalizedItem);
332
+ table.itemCount++;
333
+ }
334
+
335
+ // Salva no store
336
+ this.store.write(TableName, items);
337
+ this.persistTables();
338
+
339
+ logger.verboso(`PutItem: ${TableName}/${itemKey}`);
340
+
341
+ const response = {};
342
+ if (ReturnValues === "ALL_OLD" && oldItem) {
343
+ response.Attributes = this.marshallItem(oldItem, table);
344
+ }
345
+
346
+ return response;
347
+ }
348
+
349
+ getItem(params) {
350
+ const { TableName, Key } = params;
351
+ const table = this.tables.get(TableName);
352
+
353
+ if (!table) {
354
+ throw new Error(`Table ${TableName} does not exist`);
355
+ }
356
+
357
+ const items = this.store.read(TableName);
358
+ const itemKey = this.getItemKeyFromKeys(Key, table);
359
+
360
+ const item = items.find((item) => this.getItemKey(item, table) === itemKey);
361
+
362
+ logger.verboso(`GetItem: ${TableName}/${itemKey} - ${item ? "found" : "not found"}`);
363
+
364
+ return item ? { Item: this.marshallItem(item, table) } : {};
365
+ }
366
+
367
+ updateItem(params) {
368
+ const { TableName, Key, UpdateExpression, ExpressionAttributeNames = {}, ExpressionAttributeValues = {}, ReturnValues = "NONE" } = params;
369
+ const table = this.tables.get(TableName);
370
+
371
+ if (!table) {
372
+ throw new Error(`Table ${TableName} does not exist`);
373
+ }
374
+
375
+ // Busca o item atual (upsert: cria se não existir, como o DynamoDB real)
376
+ const items = this.store.read(TableName);
377
+ const itemKey = this.getItemKeyFromKeys(Key, table);
378
+ const index = items.findIndex((item) => this.getItemKey(item, table) === itemKey);
379
+
380
+ // Se não existe, cria um novo item com as chaves fornecidas
381
+ if (index === -1) {
382
+ const newItem = this.normalizeItem(Key, table);
383
+ newItem._createdAt = new Date().toISOString();
384
+ newItem._updatedAt = new Date().toISOString();
385
+ if (UpdateExpression) {
386
+ this.processUpdateExpression(newItem, UpdateExpression, ExpressionAttributeNames, ExpressionAttributeValues, table);
387
+ }
388
+ items.push(newItem);
389
+ this.store.write(TableName, items);
390
+ logger.verboso(`UpdateItem (upsert): ${TableName}/${itemKey}`);
391
+ const response = {};
392
+ if (ReturnValues === "ALL_NEW" || ReturnValues === "UPDATED_NEW") {
393
+ response.Attributes = this.marshallItem(newItem, table);
394
+ }
395
+ return response;
396
+ }
397
+
398
+ const currentItem = items[index];
399
+ const updatedItem = { ...currentItem };
400
+ updatedItem._updatedAt = new Date().toISOString();
401
+
402
+ // Processa a UpdateExpression
403
+ if (UpdateExpression) {
404
+ this.processUpdateExpression(updatedItem, UpdateExpression, ExpressionAttributeNames, ExpressionAttributeValues, table);
405
+ }
406
+
407
+ // Salva o item atualizado
408
+ const oldItem = { ...items[index] };
409
+ items[index] = updatedItem;
410
+ this.store.write(TableName, items);
411
+
412
+ logger.verboso(`UpdateItem: ${TableName}/${itemKey}`);
413
+
414
+ const response = {};
415
+ switch (ReturnValues) {
416
+ case "ALL_OLD":
417
+ response.Attributes = this.marshallItem(oldItem, table);
418
+ break;
419
+ case "ALL_NEW":
420
+ case "UPDATED_NEW":
421
+ response.Attributes = this.marshallItem(updatedItem, table);
422
+ break;
423
+ case "UPDATED_OLD":
424
+ response.Attributes = this.marshallItem(oldItem, table);
425
+ break;
426
+ default:
427
+ break;
428
+ }
429
+
430
+ return response;
431
+ }
432
+
433
+ deleteItem(params) {
434
+ const { TableName, Key, ReturnValues = "NONE" } = params;
435
+ const table = this.tables.get(TableName);
436
+
437
+ if (!table) {
438
+ throw new Error(`Table ${TableName} does not exist`);
439
+ }
440
+
441
+ const items = this.store.read(TableName);
442
+ const itemKey = this.getItemKeyFromKeys(Key, table);
443
+ const index = items.findIndex((item) => this.getItemKey(item, table) === itemKey);
444
+
445
+ if (index === -1) {
446
+ return {};
447
+ }
448
+
449
+ const oldItem = { ...items[index] };
450
+ items.splice(index, 1);
451
+ this.store.write(TableName, items);
452
+ table.itemCount--;
453
+ this.persistTables();
454
+
455
+ logger.verboso(`DeleteItem: ${TableName}/${itemKey}`);
456
+
457
+ const response = {};
458
+ if (ReturnValues === "ALL_OLD") {
459
+ response.Attributes = this.marshallItem(oldItem, table);
460
+ }
461
+
462
+ return response;
463
+ }
464
+
465
+ batchWriteItem(params) {
466
+ const { RequestItems } = params;
467
+ const responses = {};
468
+
469
+ if (!RequestItems) {
470
+ logger.debug(`[DEBUG batchWriteItem] params recebido: ${JSON.stringify(params)}`);
471
+ throw new Error(`RequestItems is required for BatchWriteItem. Params received: ${JSON.stringify(params)}`);
472
+ }
473
+
474
+ // Serializa por tabela usando o mutex para evitar race condition
475
+ const tablePromises = Object.entries(RequestItems).map(([tableName, operations]) =>
476
+ this._withTableLock(tableName, () => {
477
+ const table = this.tables.get(tableName);
478
+ if (!table) {
479
+ logger.info(`[BATCH-DEBUG] tabela não encontrada: ${tableName}`);
480
+ return;
481
+ }
482
+
483
+ const itemsBefore = this.store.read(tableName);
484
+ logger.info(`[BATCH-DEBUG] ${tableName} | antes=${itemsBefore.length} | ops=${operations.length}`);
485
+
486
+ let items = [...itemsBefore];
487
+ const unprocessedItems = [];
488
+ let inserts = 0;
489
+ let updates = 0;
490
+
491
+ for (const op of operations) {
492
+ if (op.PutRequest) {
493
+ const item = this.normalizeItem(op.PutRequest.Item, table);
494
+ const itemKey = this.getItemKey(item, table);
495
+ const index = items.findIndex((i) => this.getItemKey(i, table) === itemKey);
496
+
497
+ if (index !== -1) {
498
+ items[index] = item;
499
+ updates++;
500
+ } else {
501
+ items.push(item);
502
+ table.itemCount++;
503
+ inserts++;
504
+ }
505
+ } else if (op.DeleteRequest) {
506
+ const key = op.DeleteRequest.Key;
507
+ const itemKey = this.getItemKeyFromKeys(key, table);
508
+ const index = items.findIndex((i) => this.getItemKey(i, table) === itemKey);
509
+
510
+ if (index !== -1) {
511
+ items.splice(index, 1);
512
+ table.itemCount--;
513
+ } else {
514
+ unprocessedItems.push(op);
515
+ }
516
+ }
517
+ }
518
+
519
+ this.store.write(tableName, items);
520
+ const itemsAfter = this.store.read(tableName);
521
+ logger.info(`[BATCH-DEBUG] ${tableName} | depois=${itemsAfter.length} | esperado=${items.length} | match=${itemsAfter.length === items.length} | inserts=${inserts} | updates=${updates}`);
522
+
523
+ if (unprocessedItems.length > 0) {
524
+ responses[tableName] = unprocessedItems;
525
+ }
526
+ })
527
+ );
528
+
529
+ return Promise.all(tablePromises).then(() => {
530
+ this.persistTables();
531
+ const finalCount = this.store.read(Object.keys(RequestItems)[0]).length;
532
+ //logger.info(`[BATCH-DEBUG] FINAL | tabela=${Object.keys(RequestItems)[0]} | total=${finalCount}`);
533
+ return { UnprocessedItems: responses };
534
+ });
535
+ }
536
+
537
+ batchGetItem(params) {
538
+ const { RequestItems } = params;
539
+ const responses = {};
540
+
541
+ for (const [tableName, request] of Object.entries(RequestItems)) {
542
+ const table = this.tables.get(tableName);
543
+ if (!table) continue;
544
+
545
+ const items = this.store.read(tableName);
546
+ const { Keys } = request;
547
+ const foundItems = [];
548
+
549
+ for (const key of Keys) {
550
+ const itemKey = this.getItemKeyFromKeys(key, table);
551
+ const item = items.find((i) => this.getItemKey(i, table) === itemKey);
552
+ if (item) {
553
+ foundItems.push(this.marshallItem(item, table));
554
+ }
555
+ }
556
+
557
+ responses[tableName] = { Items: foundItems };
558
+ }
559
+
560
+ return { Responses: responses };
561
+ }
562
+
563
+ query(params) {
564
+ const {
565
+ TableName,
566
+ KeyConditionExpression,
567
+ FilterExpression,
568
+ ExpressionAttributeValues,
569
+ ExpressionAttributeNames = {},
570
+ IndexName,
571
+ Limit,
572
+ ExclusiveStartKey,
573
+ ProjectionExpression,
574
+ ScanIndexForward = true
575
+ } = params;
576
+ const table = this.tables.get(TableName);
577
+
578
+ if (!table) {
579
+ throw new Error(`Table ${TableName} does not exist`);
580
+ }
581
+
582
+ let items = this.store.read(TableName);
583
+
584
+ // Se for consulta por índice, filtra itens que não possuem as chaves do índice (Sparse Index)
585
+ if (IndexName && table.globalSecondaryIndexes?.[IndexName]) {
586
+ const gsi = table.globalSecondaryIndexes[IndexName];
587
+ items = items.filter(item => item[gsi.hashKey] !== undefined);
588
+ if (gsi.rangeKey) {
589
+ items = items.filter(item => item[gsi.rangeKey] !== undefined);
590
+ }
591
+ }
592
+
593
+ // Helper para resolver nomes de atributos (que podem ser placeholders como #n0)
594
+ const resolveAttributeName = (name) => {
595
+ if (name.startsWith("#")) return ExpressionAttributeNames[name] || name;
596
+ return name;
597
+ };
598
+
599
+ // Helper para extrair valor de placeholder (ex: :v0)
600
+ const resolveValue = (placeholder) => {
601
+ const rawValue = ExpressionAttributeValues[placeholder];
602
+ if (rawValue === undefined) return undefined;
603
+ if (rawValue !== null && typeof rawValue === 'object' && !Array.isArray(rawValue)) {
604
+ const keys = Object.keys(rawValue);
605
+ if (keys.length === 1 && ["S", "N", "BOOL", "NULL", "M", "L", "SS", "NS", "BS"].includes(keys[0])) {
606
+ return this.normalizeValue(rawValue, table);
607
+ }
608
+ }
609
+ return rawValue;
610
+ };
611
+
612
+ // Filtra pela KeyConditionExpression
613
+ if (KeyConditionExpression) {
614
+ const parts = KeyConditionExpression.split(/\s+AND\s+/i);
615
+ for (const part of parts) {
616
+ const trimmedPart = part.trim();
617
+
618
+ // Tenta match de função: begins_with(attr, :val)
619
+ const funcMatch = trimmedPart.match(/^begins_with\s*\(\s*([^\s,]+)\s*,\s*([^\s,)]+)\s*\)$/i);
620
+ if (funcMatch) {
621
+ const attributeName = resolveAttributeName(funcMatch[1]);
622
+ const expectedValue = resolveValue(funcMatch[2]);
623
+ items = items.filter(item => String(item[attributeName] || "").startsWith(String(expectedValue)));
624
+ continue;
625
+ }
626
+
627
+ // Tenta match de operador infix: attr OP :val
628
+ const match = trimmedPart.match(/([^\s]+)\s*(=|>|<|>=|<=|BEGINS_WITH|BETWEEN)\s*([^\s]+)(?:\s+AND\s+([^\s]+))?/i);
629
+ if (match) {
630
+ const attrPlaceholder = match[1];
631
+ const operator = match[2].toUpperCase();
632
+ const valPlaceholder = match[3];
633
+ const attributeName = resolveAttributeName(attrPlaceholder);
634
+ const expectedValue = resolveValue(valPlaceholder);
635
+
636
+ if (operator === "=") items = items.filter(item => item[attributeName] === expectedValue);
637
+ else if (operator === ">") items = items.filter(item => item[attributeName] > expectedValue);
638
+ else if (operator === "<") items = items.filter(item => item[attributeName] < expectedValue);
639
+ else if (operator === ">=") items = items.filter(item => item[attributeName] >= expectedValue);
640
+ else if (operator === "<=") items = items.filter(item => item[attributeName] <= expectedValue);
641
+ else if (operator === "BEGINS_WITH") {
642
+ const val = expectedValue;
643
+ items = items.filter(item => String(item[attributeName] || "").startsWith(String(val)));
644
+ }
645
+ }
646
+ }
647
+ }
648
+
649
+ // Ordenação (DynamoDB sempre ordena pela Sort Key)
650
+ let sortKey = table.rangeKey;
651
+ if (IndexName && table.globalSecondaryIndexes?.[IndexName]) {
652
+ sortKey = table.globalSecondaryIndexes[IndexName].rangeKey;
653
+ }
654
+
655
+ if (sortKey) {
656
+ items.sort((a, b) => {
657
+ const valA = a[sortKey];
658
+ const valB = b[sortKey];
659
+
660
+ if (valA === valB) return 0;
661
+ if (valA === undefined || valA === null) return 1;
662
+ if (valB === undefined || valB === null) return -1;
663
+
664
+ let comparison = 0;
665
+ if (typeof valA === 'number' && typeof valB === 'number') {
666
+ comparison = valA - valB;
667
+ } else {
668
+ comparison = String(valA).localeCompare(String(valB));
669
+ }
670
+
671
+ return ScanIndexForward ? comparison : -comparison;
672
+ });
673
+ }
674
+
675
+ const scannedCount = items.length;
676
+
677
+ // Aplica FilterExpression se existir
678
+ if (FilterExpression) {
679
+ items = this.applyFilter(items, FilterExpression, ExpressionAttributeValues, ExpressionAttributeNames, table);
680
+ }
681
+
682
+ const totalMatchingCount = items.length;
683
+
684
+ // Apply Pagination (ExclusiveStartKey)
685
+ if (ExclusiveStartKey) {
686
+ const startKeyStr = this.getItemKeyFromKeys(ExclusiveStartKey, table);
687
+ const startIndex = items.findIndex(item => this.getItemKey(item, table) === startKeyStr);
688
+ if (startIndex !== -1) {
689
+ items = items.slice(startIndex + 1);
690
+ }
691
+ }
692
+
693
+ // Apply Limit
694
+ let lastEvaluatedKey = null;
695
+ if (Limit && items.length > Limit) {
696
+ const lastItem = items[Limit - 1];
697
+ lastEvaluatedKey = this.marshallItem(lastItem, table);
698
+ items = items.slice(0, Limit);
699
+ }
700
+
701
+ let marshalledItems = items.map((item) => this.marshallItem(item, table));
702
+
703
+ // Apply Projection
704
+ if (ProjectionExpression) {
705
+ marshalledItems = this.applyProjection(marshalledItems, ProjectionExpression, ExpressionAttributeNames);
706
+ }
707
+
708
+ return {
709
+ Items: marshalledItems,
710
+ Count: marshalledItems.length,
711
+ ScannedCount: scannedCount,
712
+ LastEvaluatedKey: lastEvaluatedKey || undefined
713
+ };
714
+ }
715
+
716
+ scan(params) {
717
+ const {
718
+ TableName,
719
+ FilterExpression,
720
+ ExpressionAttributeValues,
721
+ ExpressionAttributeNames = {},
722
+ Limit,
723
+ ExclusiveStartKey,
724
+ ProjectionExpression
725
+ } = params;
726
+ const table = this.tables.get(TableName);
727
+
728
+ if (!table) {
729
+ throw new Error(`Table ${TableName} does not exist`);
730
+ }
731
+
732
+ const allItems = this.store.read(TableName);
733
+ let items = allItems;
734
+ const scannedCount = items.length;
735
+
736
+ // Aplica filtro se existir
737
+ if (FilterExpression) {
738
+ items = this.applyFilter(items, FilterExpression, ExpressionAttributeValues, ExpressionAttributeNames, table);
739
+ }
740
+
741
+ // Apply Pagination (ExclusiveStartKey)
742
+ if (ExclusiveStartKey) {
743
+ const startKeyStr = this.getItemKeyFromKeys(ExclusiveStartKey, table);
744
+ const startIndex = items.findIndex(item => this.getItemKey(item, table) === startKeyStr);
745
+ if (startIndex !== -1) {
746
+ items = items.slice(startIndex + 1);
747
+ }
748
+ }
749
+
750
+ // Apply Limit
751
+ let lastEvaluatedKey = null;
752
+ if (Limit && items.length > Limit) {
753
+ const lastItem = items[Limit - 1];
754
+ lastEvaluatedKey = this.marshallItem(lastItem, table);
755
+ items = items.slice(0, Limit);
756
+ }
757
+
758
+ let marshalledItems = items.map((item) => this.marshallItem(item, table));
759
+
760
+ // Apply Projection
761
+ if (ProjectionExpression) {
762
+ marshalledItems = this.applyProjection(marshalledItems, ProjectionExpression, ExpressionAttributeNames);
763
+ }
764
+
765
+ return {
766
+ Items: marshalledItems,
767
+ Count: marshalledItems.length,
768
+ ScannedCount: scannedCount,
769
+ LastEvaluatedKey: lastEvaluatedKey || undefined
770
+ };
771
+ }
772
+
773
+ applyProjection(items, expression, names = {}) {
774
+ const projectedAttrs = expression.split(',').map(s => s.trim()).filter(Boolean);
775
+ const resolvedAttrs = projectedAttrs.map(attr => {
776
+ if (attr.startsWith("#")) return names[attr] || attr;
777
+ return attr;
778
+ });
779
+
780
+ return items.map(item => {
781
+ const newItem = {};
782
+ resolvedAttrs.forEach(attr => {
783
+ if (item[attr] !== undefined) {
784
+ newItem[attr] = item[attr];
785
+ }
786
+ });
787
+ return newItem;
788
+ });
789
+ }
790
+
791
+
792
+ // Métodos auxiliares
793
+ normalizeItem(item, table) {
794
+ const normalized = { ...item };
795
+
796
+ for (const [key, value] of Object.entries(normalized)) {
797
+ normalized[key] = this.normalizeValue(value, table);
798
+ }
799
+
800
+ return normalized;
801
+ }
802
+
803
+ normalizeValue(value, table) {
804
+ if (value === null || value === undefined) return value;
805
+ if (typeof value !== 'object') return value;
806
+
807
+ if (value.S !== undefined) return value.S;
808
+ if (value.N !== undefined) return parseFloat(value.N);
809
+ if (value.BOOL !== undefined) return value.BOOL;
810
+ if (value.NULL !== undefined) return null;
811
+ if (value.L !== undefined) return value.L.map((v) => this.normalizeValue(v, table));
812
+ if (value.M !== undefined) return this.normalizeItem(value.M, table);
813
+ if (value.SS !== undefined) return value.SS;
814
+ if (value.NS !== undefined) return value.NS.map(Number);
815
+
816
+ // plain object (already normalized, e.g. stored without DynamoDB types)
817
+ return value;
818
+ }
819
+
820
+ marshallValue(value, table) {
821
+ if (value === null || value === undefined) return { NULL: true };
822
+ if (typeof value === 'boolean') return { BOOL: value };
823
+ if (typeof value === 'number') return { N: String(value) };
824
+ if (typeof value === 'string') return { S: value };
825
+ if (Array.isArray(value)) return { L: value.map((v) => this.marshallValue(v, table)) };
826
+ if (typeof value === 'object') return { M: this.marshallItem(value, table) };
827
+ return { S: String(value) };
828
+ }
829
+
830
+ marshallItem(item, table) {
831
+ const marshalled = {};
832
+
833
+ for (const [key, value] of Object.entries(item)) {
834
+ if (key.startsWith("_")) continue; // Pula campos internos
835
+
836
+ const type = table ? table.attributeTypes[key] : null;
837
+ if (type === "S") {
838
+ marshalled[key] = { S: String(value) };
839
+ } else if (type === "N") {
840
+ marshalled[key] = { N: String(value) };
841
+ } else {
842
+ marshalled[key] = this.marshallValue(value, table);
843
+ }
844
+ }
845
+
846
+ return marshalled;
847
+ }
848
+
849
+ getItemKey(item, table) {
850
+ const hashValue = item[table.hashKey];
851
+ const rangeValue = table.rangeKey ? item[table.rangeKey] : null;
852
+ return rangeValue ? `${hashValue}|${rangeValue}` : String(hashValue);
853
+ }
854
+
855
+ getItemKeyFromKeys(keys, table) {
856
+ const rawHash = keys[table.hashKey];
857
+ const hashValue = rawHash && typeof rawHash === 'object' ? Object.values(rawHash)[0] : rawHash;
858
+ const rawRange = table.rangeKey ? keys[table.rangeKey] : null;
859
+ const rangeValue = rawRange && typeof rawRange === 'object' ? Object.values(rawRange)[0] : rawRange;
860
+ return rangeValue ? `${hashValue}|${rangeValue}` : String(hashValue);
861
+ }
862
+
863
+ processUpdateExpression(item, expression, nameMap, valueMap, table) {
864
+ // SET clause
865
+ const setMatch = expression.match(/SET\s+([^]+?)(?=\s+(?:REMOVE|ADD|DELETE)\s|\s*$)/i);
866
+ if (setMatch) {
867
+ const assignments = setMatch[1].split(",").map((a) => a.trim());
868
+ for (const assignment of assignments) {
869
+ const [path, valueExpr] = assignment.split("=").map((s) => s.trim());
870
+ const attributeName = nameMap[path] || path.replace(/#/g, "");
871
+ const rawValue = valueMap[valueExpr];
872
+ // Usa normalizeValue para garantir o mesmo formato que o putItem
873
+ item[attributeName] = this.normalizeValue(rawValue, table);
874
+ }
875
+ }
876
+
877
+ // ADD clause incrementa números ou adiciona a sets (upsert-friendly)
878
+ const addMatch = expression.match(/ADD\s+([^]+?)(?=\s+(?:SET|REMOVE|DELETE)\s|\s*$)/i);
879
+ if (addMatch) {
880
+ const assignments = addMatch[1].split(",").map((a) => a.trim());
881
+ for (const assignment of assignments) {
882
+ const parts = assignment.split(/\s+/);
883
+ const attributeName = nameMap[parts[0]] || parts[0].replace(/#/g, "");
884
+ const rawValue = valueMap[parts[1]];
885
+ // Usa normalizeValue para garantir o mesmo formato que o putItem
886
+ const delta = this.normalizeValue(rawValue, table);
887
+ const current = item[attributeName];
888
+ if (current === undefined || current === null) {
889
+ item[attributeName] = typeof delta === 'number' ? delta : parseFloat(delta) || 0;
890
+ } else {
891
+ item[attributeName] = (parseFloat(current) || 0) + (parseFloat(delta) || 0);
892
+ }
893
+ }
894
+ }
895
+
896
+ // REMOVE clause
897
+ const removeMatch = expression.match(/REMOVE\s+([^]+?)(?=\s+(?:SET|ADD|DELETE)\s|\s*$)/i);
898
+ if (removeMatch) {
899
+ const attributes = removeMatch[1].split(",").map((a) => a.trim());
900
+ for (const attr of attributes) {
901
+ const attributeName = nameMap[attr] || attr.replace(/#/g, "");
902
+ delete item[attributeName];
903
+ }
904
+ }
905
+ }
906
+
907
+ applyFilter(items, expression, values, names, table) {
908
+ if (!expression) return items;
909
+
910
+ const resolveAttributeName = (name) => {
911
+ if (name.startsWith("#")) return names[name] || name;
912
+ return name;
913
+ };
914
+
915
+ const resolveValue = (placeholder) => {
916
+ const rawValue = values[placeholder];
917
+ if (rawValue === undefined) return undefined;
918
+ if (rawValue !== null && typeof rawValue === 'object' && !Array.isArray(rawValue)) {
919
+ const keys = Object.keys(rawValue);
920
+ if (keys.length === 1 && ["S", "N", "BOOL", "NULL", "M", "L", "SS", "NS", "BS"].includes(keys[0])) {
921
+ return this.normalizeValue(rawValue, table);
922
+ }
923
+ }
924
+ return rawValue;
925
+ };
926
+
927
+ const conditions = expression.split(/\s+AND\s+/i);
928
+
929
+ return items.filter((item) => {
930
+ return conditions.every(cond => {
931
+ // Regex para match de funções como contains(#n, :v) ou begins_with(#n, :v)
932
+ const funcMatch = cond.match(/(contains|begins_with)\s*\(\s*([^\s,]+)\s*,\s*([^\s,)]+)\s*\)/i);
933
+ if (funcMatch) {
934
+ const func = funcMatch[1].toLowerCase();
935
+ const attrName = resolveAttributeName(funcMatch[2]);
936
+ const expectedVal = resolveValue(funcMatch[3]);
937
+ const actualVal = item[attrName];
938
+
939
+ if (func === 'contains') {
940
+ if (Array.isArray(actualVal)) return actualVal.includes(expectedVal);
941
+ return String(actualVal || "").includes(String(expectedVal));
942
+ }
943
+ if (func === 'begins_with') {
944
+ return String(actualVal || "").startsWith(String(expectedVal));
945
+ }
946
+ }
947
+
948
+ // Regex para operadores básicos
949
+ const opMatch = cond.match(/([^\s]+)\s*(=|<>|<|<=|>|>=)\s*([^\s]+)/);
950
+ if (opMatch) {
951
+ const attrName = resolveAttributeName(opMatch[1]);
952
+ const operator = opMatch[2];
953
+ const expectedVal = resolveValue(opMatch[3]);
954
+ const actualVal = item[attrName];
955
+
956
+ switch (operator) {
957
+ case "=": return actualVal === expectedVal;
958
+ case "<>": return actualVal !== expectedVal;
959
+ case "<": return actualVal < expectedVal;
960
+ case "<=": return actualVal <= expectedVal;
961
+ case ">": return actualVal > expectedVal;
962
+ case ">=": return actualVal >= expectedVal;
963
+ default: return true;
964
+ }
965
+ }
966
+ return true;
967
+ });
968
+ });
969
+ }
970
+
971
+
972
+ persistTables() {
973
+ const tablesObj = {};
974
+ for (const [name, table] of this.tables.entries()) {
975
+ tablesObj[name] = table;
976
+ }
977
+ this.store.write("__tables__", tablesObj);
978
+ }
979
+
980
+ async reset() {
981
+ for (const [tableName] of this.tables) {
982
+ this.store.write(tableName, []);
983
+ }
984
+ logger.debug("DynamoDB: Todos os dados resetados");
985
+ }
986
+
987
+ getTablesCount() {
988
+ return this.tables.size;
989
+ }
990
+
991
+ getTotalItems() {
992
+ let total = 0;
993
+ for (const [tableName] of this.tables) {
994
+ const items = this.store.read(tableName);
995
+ total += items.length;
996
+ }
997
+ return total;
998
+ }
999
+
1000
+ listTables() {
1001
+ return { TableNames: Array.from(this.tables.keys()) };
1002
+ }
1003
+ }
1004
+
1005
+ module.exports = DynamoDBSimulator;