@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
@@ -0,0 +1,722 @@
1
+ /**
2
+ * SQLite Store - Um arquivo .db por tabela + __tables__.json para metadados
3
+ * Mantém compatibilidade 100% com LocalStore API
4
+ * Com índices otimizados para DynamoDB queries
5
+ */
6
+
7
+ const Database = require('better-sqlite3');
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+ const crypto = require('crypto');
11
+ const logger = require('../../utils/logger');
12
+
13
+ class SQLiteStore {
14
+ constructor(dataDir) {
15
+ this.dataDir = dataDir;
16
+ this.databases = new Map();
17
+ this.metadataFile = path.join(dataDir, '__tables__.json');
18
+ this.preparedStatements = new Map(); // Cache de statements preparados
19
+
20
+ if (!fs.existsSync(dataDir)) {
21
+ fs.mkdirSync(dataDir, { recursive: true });
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Carrega metadados do arquivo JSON (compatível com LocalStore)
27
+ */
28
+ loadMetadata() {
29
+ if (fs.existsSync(this.metadataFile)) {
30
+ try {
31
+ const data = fs.readFileSync(this.metadataFile, 'utf8');
32
+ return JSON.parse(data);
33
+ } catch (error) {
34
+ logger.warn(`Erro ao ler ${this.metadataFile}: ${error.message}`);
35
+ return {};
36
+ }
37
+ }
38
+ return {};
39
+ }
40
+
41
+ /**
42
+ * Salva metadados no arquivo JSON (compatível com LocalStore)
43
+ */
44
+ saveMetadata(metadata) {
45
+ try {
46
+ fs.writeFileSync(this.metadataFile, JSON.stringify(metadata, null, 2), 'utf8');
47
+ logger.verboso(`Metadados salvos em ${this.metadataFile}`);
48
+ } catch (error) {
49
+ logger.error(`Erro ao salvar ${this.metadataFile}: ${error.message}`);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Obtém ou cria conexão com banco da tabela
55
+ */
56
+ getDatabase(tableName) {
57
+ if (this.databases.has(tableName)) {
58
+ return this.databases.get(tableName);
59
+ }
60
+
61
+ const dbPath = path.join(this.dataDir, `${tableName}.db`);
62
+ const isNew = !fs.existsSync(dbPath);
63
+
64
+ const db = new Database(dbPath);
65
+
66
+ // Otimizações de performance
67
+ db.pragma('journal_mode = WAL');
68
+ db.pragma('cache_size = -20000'); // 20MB cache
69
+ db.pragma('synchronous = NORMAL');
70
+ db.pragma('temp_store = MEMORY');
71
+ db.pragma('mmap_size = 268435456'); // 256MB mmap
72
+ db.pragma('page_size = 4096');
73
+
74
+ this.databases.set(tableName, db);
75
+
76
+ if (isNew) {
77
+ this.initTableDatabase(db, tableName);
78
+ } else {
79
+ this.prepareStatements(db, tableName);
80
+ }
81
+
82
+ return db;
83
+ }
84
+
85
+ /**
86
+ * Prepara statements otimizados para a tabela
87
+ */
88
+ prepareStatements(db, tableName) {
89
+ const statementCache = new Map();
90
+
91
+ statementCache.set('getItem', db.prepare(`
92
+ SELECT data, _created_at, _updated_at
93
+ FROM items
94
+ WHERE pk_hash = ? AND (pk_range = ? OR (pk_range IS NULL AND ? IS NULL))
95
+ `));
96
+
97
+ statementCache.set('putItem', db.prepare(`
98
+ INSERT OR REPLACE INTO items (pk_hash, pk_range, data, _created_at, _updated_at)
99
+ VALUES (?, ?, ?, ?, ?)
100
+ `));
101
+
102
+ statementCache.set('deleteItem', db.prepare(`
103
+ DELETE FROM items
104
+ WHERE pk_hash = ? AND (pk_range = ? OR (pk_range IS NULL AND ? IS NULL))
105
+ `));
106
+
107
+ statementCache.set('countItems', db.prepare(`SELECT COUNT(*) as count FROM items`));
108
+ statementCache.set('truncate', db.prepare(`DELETE FROM items`));
109
+ statementCache.set('tableSize', db.prepare(`SELECT SUM(LENGTH(data)) as size FROM items`));
110
+
111
+ this.preparedStatements.set(tableName, statementCache);
112
+ }
113
+
114
+ /**
115
+ * Inicializa schema do banco da tabela
116
+ */
117
+ initTableDatabase(db, tableName) {
118
+ db.exec(`
119
+ CREATE TABLE IF NOT EXISTS items (
120
+ pk_hash TEXT NOT NULL,
121
+ pk_range TEXT,
122
+ data TEXT NOT NULL,
123
+ _created_at TEXT NOT NULL,
124
+ _updated_at TEXT NOT NULL,
125
+ PRIMARY KEY (pk_hash, pk_range)
126
+ );
127
+
128
+ CREATE INDEX IF NOT EXISTS idx_items_updated
129
+ ON items (_updated_at DESC);
130
+
131
+ CREATE INDEX IF NOT EXISTS idx_items_pk_hash
132
+ ON items (pk_hash);
133
+
134
+ CREATE INDEX IF NOT EXISTS idx_items_pk_range
135
+ ON items (pk_range);
136
+ `);
137
+
138
+ this.prepareStatements(db, tableName);
139
+ logger.debug(`Banco de dados inicializado: ${tableName}.db`);
140
+ }
141
+
142
+ /**
143
+ * Sanitiza nome para uso em SQL
144
+ */
145
+ sanitizeIdentifier(name) {
146
+ return name.replace(/[^a-zA-Z0-9_]/g, '_');
147
+ }
148
+
149
+ /**
150
+ * Extrai valor de atributo do DynamoDB (suporta tipos S/N)
151
+ */
152
+ extractAttributeValue(attribute) {
153
+ if (!attribute || typeof attribute !== 'object') return attribute;
154
+
155
+ if (attribute.S !== undefined) return attribute.S;
156
+ if (attribute.N !== undefined) return parseFloat(attribute.N);
157
+ if (attribute.BOOL !== undefined) return attribute.BOOL;
158
+ if (attribute.NULL !== undefined) return null;
159
+
160
+ return attribute;
161
+ }
162
+
163
+ /**
164
+ * Cria índices para Global Secondary Indexes
165
+ */
166
+ createGSIIndices(db, tableName, globalSecondaryIndexes) {
167
+ if (!globalSecondaryIndexes) return;
168
+
169
+ for (const [indexName, gsi] of Object.entries(globalSecondaryIndexes)) {
170
+ try {
171
+ const safeIndexName = this.sanitizeIdentifier(indexName);
172
+ const hashKey = this.sanitizeIdentifier(gsi.hashKey);
173
+
174
+ // Drop existing index if any
175
+ db.exec(`DROP INDEX IF EXISTS idx_gsi_${safeIndexName}`);
176
+
177
+ if (gsi.rangeKey) {
178
+ const rangeKey = this.sanitizeIdentifier(gsi.rangeKey);
179
+ const indexSQL = `
180
+ CREATE INDEX IF NOT EXISTS idx_gsi_${safeIndexName}
181
+ ON items (json_extract(data, '$."${hashKey}"'), json_extract(data, '$."${rangeKey}"'))
182
+ WHERE json_extract(data, '$."${hashKey}"') IS NOT NULL
183
+ `;
184
+ db.exec(indexSQL);
185
+ logger.debug(`Índice GSI composto criado: ${tableName}.${indexName}`);
186
+ } else {
187
+ const indexSQL = `
188
+ CREATE INDEX IF NOT EXISTS idx_gsi_${safeIndexName}
189
+ ON items (json_extract(data, '$."${hashKey}"'))
190
+ WHERE json_extract(data, '$."${hashKey}"') IS NOT NULL
191
+ `;
192
+ db.exec(indexSQL);
193
+ logger.debug(`Índice GSI simples criado: ${tableName}.${indexName}`);
194
+ }
195
+
196
+ // Índice adicional para queries com begins_with
197
+ if (gsi.rangeKey) {
198
+ const rangeKey = this.sanitizeIdentifier(gsi.rangeKey);
199
+ db.exec(`
200
+ CREATE INDEX IF NOT EXISTS idx_gsi_${safeIndexName}_prefix
201
+ ON items (json_extract(data, '$."${hashKey}"'), json_extract(data, '$."${rangeKey}"') COLLATE NOCASE)
202
+ `);
203
+ }
204
+ } catch (error) {
205
+ logger.warn(`Erro ao criar índice GSI ${indexName}: ${error.message}`);
206
+ }
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Cria tabela (API compatível com LocalStore)
212
+ */
213
+ createTable(tableDef) {
214
+ const metadata = this.loadMetadata();
215
+
216
+ metadata[tableDef.name] = {
217
+ name: tableDef.name,
218
+ hashKey: tableDef.hashKey,
219
+ rangeKey: tableDef.rangeKey,
220
+ attributeTypes: tableDef.attributeTypes,
221
+ globalSecondaryIndexes: tableDef.globalSecondaryIndexes,
222
+ createdAt: tableDef.createdAt || new Date().toISOString(),
223
+ itemCount: tableDef.itemCount || 0,
224
+ sizeBytes: tableDef.sizeBytes || 0
225
+ };
226
+
227
+ this.saveMetadata(metadata);
228
+
229
+ const db = this.getDatabase(tableDef.name);
230
+
231
+ if (tableDef.globalSecondaryIndexes && Object.keys(tableDef.globalSecondaryIndexes).length > 0) {
232
+ this.createGSIIndices(db, tableDef.name, tableDef.globalSecondaryIndexes);
233
+ }
234
+
235
+ logger.debug(`Tabela criada: ${tableDef.name}.db`);
236
+ return metadata[tableDef.name];
237
+ }
238
+
239
+ /**
240
+ * Obtém definição da tabela
241
+ */
242
+ getTable(tableName) {
243
+ const metadata = this.loadMetadata();
244
+ return metadata[tableName] || null;
245
+ }
246
+
247
+ /**
248
+ * Obtém todas as tabelas
249
+ */
250
+ getAllTables() {
251
+ const metadata = this.loadMetadata();
252
+ const tables = new Map();
253
+
254
+ for (const [tableName, tableDef] of Object.entries(metadata)) {
255
+ tables.set(tableName, tableDef);
256
+ }
257
+
258
+ return tables;
259
+ }
260
+
261
+ /**
262
+ * Verifica se tabela existe
263
+ */
264
+ exists(tableName) {
265
+ const metadata = this.loadMetadata();
266
+ return !!metadata[tableName];
267
+ }
268
+
269
+ /**
270
+ * Remove tabela
271
+ */
272
+ deleteTable(tableName) {
273
+ const metadata = this.loadMetadata();
274
+ delete metadata[tableName];
275
+ this.saveMetadata(metadata);
276
+
277
+ if (this.databases.has(tableName)) {
278
+ this.databases.get(tableName).close();
279
+ this.databases.delete(tableName);
280
+ }
281
+
282
+ this.preparedStatements.delete(tableName);
283
+
284
+ const dbPath = path.join(this.dataDir, `${tableName}.db`);
285
+ if (fs.existsSync(dbPath)) {
286
+ fs.unlinkSync(dbPath);
287
+ logger.debug(`Arquivo removido: ${tableName}.db`);
288
+ }
289
+
290
+ const walPath = path.join(this.dataDir, `${tableName}.db-wal`);
291
+ if (fs.existsSync(walPath)) fs.unlinkSync(walPath);
292
+
293
+ const shmPath = path.join(this.dataDir, `${tableName}.db-shm`);
294
+ if (fs.existsSync(shmPath)) fs.unlinkSync(shmPath);
295
+
296
+ logger.debug(`Tabela removida: ${tableName}`);
297
+ }
298
+
299
+ /**
300
+ * Atualiza estatísticas da tabela
301
+ */
302
+ updateTableStats(tableName, itemCount, sizeBytes) {
303
+ const metadata = this.loadMetadata();
304
+
305
+ if (metadata[tableName]) {
306
+ metadata[tableName].itemCount = itemCount;
307
+ metadata[tableName].sizeBytes = sizeBytes;
308
+ this.saveMetadata(metadata);
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Insere ou atualiza item (API otimizada)
314
+ */
315
+ putItem(tableName, pkHash, pkRange, item, createdAt, updatedAt) {
316
+ const db = this.getDatabase(tableName);
317
+ const stmtCache = this.preparedStatements.get(tableName);
318
+ const stmt = stmtCache ? stmtCache.get('putItem') :
319
+ db.prepare(`INSERT OR REPLACE INTO items (pk_hash, pk_range, data, _created_at, _updated_at) VALUES (?, ?, ?, ?, ?)`);
320
+
321
+ // Remove campos internos do data
322
+ const { _createdAt, _updatedAt, ...itemData } = item;
323
+
324
+ stmt.run(
325
+ pkHash,
326
+ pkRange || null,
327
+ JSON.stringify(itemData),
328
+ createdAt || updatedAt,
329
+ updatedAt
330
+ );
331
+
332
+ return { success: true };
333
+ }
334
+
335
+ /**
336
+ * Obtém item por chave
337
+ */
338
+ getItem(tableName, pkHash, pkRange) {
339
+ const db = this.getDatabase(tableName);
340
+ const stmtCache = this.preparedStatements.get(tableName);
341
+ const stmt = stmtCache ? stmtCache.get('getItem') :
342
+ db.prepare(`SELECT data, _created_at, _updated_at FROM items WHERE pk_hash = ? AND (pk_range = ? OR (pk_range IS NULL AND ? IS NULL))`);
343
+
344
+ const row = stmt.get(pkHash, pkRange, pkRange);
345
+ if (!row) return null;
346
+
347
+ const item = JSON.parse(row.data);
348
+ item._createdAt = row._created_at;
349
+ item._updatedAt = row._updated_at;
350
+
351
+ return item;
352
+ }
353
+
354
+ /**
355
+ * Remove item
356
+ */
357
+ deleteItem(tableName, pkHash, pkRange) {
358
+ const db = this.getDatabase(tableName);
359
+ const stmtCache = this.preparedStatements.get(tableName);
360
+ const stmt = stmtCache ? stmtCache.get('deleteItem') :
361
+ db.prepare(`DELETE FROM items WHERE pk_hash = ? AND (pk_range = ? OR (pk_range IS NULL AND ? IS NULL))`);
362
+
363
+ const result = stmt.run(pkHash, pkRange, pkRange);
364
+ return result.changes > 0;
365
+ }
366
+
367
+ /**
368
+ * Busca com paginação otimizada para DynamoDB Query
369
+ */
370
+ queryItemsOptimized(tableName, options = {}) {
371
+ const db = this.getDatabase(tableName);
372
+ let sql = `SELECT data, _created_at, _updated_at FROM items WHERE 1=1`;
373
+ const params = [];
374
+
375
+ // Filtro por partition key
376
+ if (options.hashKeyValue !== undefined) {
377
+ sql += ` AND pk_hash = ?`;
378
+ params.push(String(options.hashKeyValue));
379
+ }
380
+
381
+ // Filtro por sort key (suporta begins_with, between, comparison)
382
+ if (options.rangeKeyCondition) {
383
+ const { operator, value, value2 } = options.rangeKeyCondition;
384
+
385
+ switch (operator) {
386
+ case '=':
387
+ sql += ` AND pk_range = ?`;
388
+ params.push(String(value));
389
+ break;
390
+ case '>':
391
+ sql += ` AND pk_range > ?`;
392
+ params.push(String(value));
393
+ break;
394
+ case '<':
395
+ sql += ` AND pk_range < ?`;
396
+ params.push(String(value));
397
+ break;
398
+ case '>=':
399
+ sql += ` AND pk_range >= ?`;
400
+ params.push(String(value));
401
+ break;
402
+ case '<=':
403
+ sql += ` AND pk_range <= ?`;
404
+ params.push(String(value));
405
+ break;
406
+ case 'BEGINS_WITH':
407
+ sql += ` AND pk_range LIKE ?`;
408
+ params.push(`${value}%`);
409
+ break;
410
+ case 'BETWEEN':
411
+ sql += ` AND pk_range BETWEEN ? AND ?`;
412
+ params.push(String(value), String(value2));
413
+ break;
414
+ }
415
+ }
416
+
417
+ // Ordenação
418
+ if (options.sortKey) {
419
+ const direction = options.scanIndexForward !== false ? 'ASC' : 'DESC';
420
+ sql += ` ORDER BY json_extract(data, '$."${options.sortKey}"') ${direction}`;
421
+ } else if (options.sortByRangeKey !== false) {
422
+ const direction = options.scanIndexForward !== false ? 'ASC' : 'DESC';
423
+ sql += ` ORDER BY pk_range ${direction}`;
424
+ }
425
+
426
+ // Paginação com ExclusiveStartKey
427
+ if (options.exclusiveStartKey) {
428
+ if (options.exclusiveStartKey.rangeKey !== undefined && options.exclusiveStartKey.rangeKey !== null) {
429
+ const op = options.scanIndexForward !== false ? '>' : '<';
430
+ sql += ` AND (pk_hash > ? OR (pk_hash = ? AND pk_range ${op} ?))`;
431
+ params.push(options.exclusiveStartKey.hashKey, options.exclusiveStartKey.hashKey, options.exclusiveStartKey.rangeKey);
432
+ } else if (options.exclusiveStartKey.hashKey) {
433
+ const op = options.scanIndexForward !== false ? '>' : '<';
434
+ sql += ` AND pk_hash ${op} ?`;
435
+ params.push(options.exclusiveStartKey.hashKey);
436
+ }
437
+ }
438
+
439
+ // Limite
440
+ if (options.limit) {
441
+ sql += ` LIMIT ?`;
442
+ params.push(options.limit + 1); // +1 para saber se tem próxima página
443
+ }
444
+
445
+ const stmt = db.prepare(sql);
446
+ const rows = stmt.all(...params);
447
+
448
+ let lastEvaluatedKey = null;
449
+ let results = rows;
450
+
451
+ if (options.limit && rows.length > options.limit) {
452
+ const lastItem = rows[options.limit - 1];
453
+ const lastItemData = JSON.parse(lastItem.data);
454
+ lastEvaluatedKey = {
455
+ hashKey: lastItem.pk_hash,
456
+ rangeKey: lastItem.pk_range
457
+ };
458
+ results = rows.slice(0, options.limit);
459
+ }
460
+
461
+ const items = results.map(row => {
462
+ const item = JSON.parse(row.data);
463
+ item._createdAt = row._created_at;
464
+ item._updatedAt = row._updated_at;
465
+ return item;
466
+ });
467
+
468
+ return { items, lastEvaluatedKey };
469
+ }
470
+
471
+ /**
472
+ * Busca com GSI otimizada
473
+ */
474
+ queryWithGSI(tableName, gsi, options = {}) {
475
+ const db = this.getDatabase(tableName);
476
+ const safeIndexName = this.sanitizeIdentifier(gsi.indexName);
477
+ const hashKey = this.sanitizeIdentifier(gsi.hashKey);
478
+
479
+ let sql = `
480
+ SELECT data, _created_at, _updated_at FROM items
481
+ WHERE json_extract(data, '$."${hashKey}"') IS NOT NULL
482
+ `;
483
+ const params = [];
484
+
485
+ // Filtro por hash key do GSI
486
+ if (options.hashKeyValue !== undefined) {
487
+ sql += ` AND json_extract(data, '$."${hashKey}"') = ?`;
488
+ params.push(options.hashKeyValue);
489
+ }
490
+
491
+ // Filtro por range key do GSI
492
+ if (gsi.rangeKey && options.rangeKeyCondition) {
493
+ const rangeKey = this.sanitizeIdentifier(gsi.rangeKey);
494
+ const { operator, value, value2 } = options.rangeKeyCondition;
495
+
496
+ switch (operator) {
497
+ case '=':
498
+ sql += ` AND json_extract(data, '$."${rangeKey}"') = ?`;
499
+ params.push(value);
500
+ break;
501
+ case '>':
502
+ sql += ` AND json_extract(data, '$."${rangeKey}"') > ?`;
503
+ params.push(value);
504
+ break;
505
+ case '<':
506
+ sql += ` AND json_extract(data, '$."${rangeKey}"') < ?`;
507
+ params.push(value);
508
+ break;
509
+ case 'BEGINS_WITH':
510
+ sql += ` AND json_extract(data, '$."${rangeKey}"') LIKE ?`;
511
+ params.push(`${value}%`);
512
+ break;
513
+ }
514
+ }
515
+
516
+ // Ordenação pelo range key do GSI
517
+ if (gsi.rangeKey) {
518
+ const rangeKey = this.sanitizeIdentifier(gsi.rangeKey);
519
+ const direction = options.scanIndexForward !== false ? 'ASC' : 'DESC';
520
+ sql += ` ORDER BY json_extract(data, '$."${rangeKey}"') ${direction}`;
521
+ }
522
+
523
+ if (options.limit) {
524
+ sql += ` LIMIT ?`;
525
+ params.push(options.limit);
526
+ }
527
+
528
+ const stmt = db.prepare(sql);
529
+ const rows = stmt.all(...params);
530
+
531
+ return rows.map(row => {
532
+ const item = JSON.parse(row.data);
533
+ item._createdAt = row._created_at;
534
+ item._updatedAt = row._updated_at;
535
+ return item;
536
+ });
537
+ }
538
+
539
+ /**
540
+ * Scan otimizado (com streaming)
541
+ */
542
+ scanTableOptimized(tableName, options = {}) {
543
+ const db = this.getDatabase(tableName);
544
+ let sql = `SELECT data, _created_at, _updated_at FROM items`;
545
+ const params = [];
546
+
547
+ // Ordenação padrão
548
+ sql += ` ORDER BY pk_hash, pk_range`;
549
+
550
+ // Paginação
551
+ if (options.exclusiveStartKey) {
552
+ if (options.exclusiveStartKey.rangeKey !== undefined && options.exclusiveStartKey.rangeKey !== null) {
553
+ sql += ` AND (pk_hash > ? OR (pk_hash = ? AND pk_range > ?))`;
554
+ params.push(options.exclusiveStartKey.hashKey, options.exclusiveStartKey.hashKey, options.exclusiveStartKey.rangeKey);
555
+ } else if (options.exclusiveStartKey.hashKey) {
556
+ sql += ` AND pk_hash > ?`;
557
+ params.push(options.exclusiveStartKey.hashKey);
558
+ }
559
+ }
560
+
561
+ if (options.limit) {
562
+ sql += ` LIMIT ?`;
563
+ params.push(options.limit);
564
+ }
565
+
566
+ const stmt = db.prepare(sql);
567
+ const rows = stmt.all(...params);
568
+
569
+ return rows.map(row => {
570
+ const item = JSON.parse(row.data);
571
+ item._createdAt = row._created_at;
572
+ item._updatedAt = row._updated_at;
573
+ return item;
574
+ });
575
+ }
576
+
577
+ /**
578
+ * Escrita em lote (transação)
579
+ */
580
+ batchWrite(tableName, operations) {
581
+ const db = this.getDatabase(tableName);
582
+
583
+ const transaction = db.transaction((ops) => {
584
+ for (const op of ops) {
585
+ if (op.type === 'put') {
586
+ this.putItem(tableName, op.pkHash, op.pkRange, op.item, op.createdAt, op.updatedAt);
587
+ } else if (op.type === 'delete') {
588
+ this.deleteItem(tableName, op.pkHash, op.pkRange);
589
+ }
590
+ }
591
+ });
592
+
593
+ return transaction(operations);
594
+ }
595
+
596
+ /**
597
+ * Limpa todos os dados da tabela
598
+ */
599
+ truncateTable(tableName) {
600
+ const db = this.getDatabase(tableName);
601
+ const stmtCache = this.preparedStatements.get(tableName);
602
+ const stmt = stmtCache ? stmtCache.get('truncate') : db.prepare(`DELETE FROM items`);
603
+
604
+ const result = stmt.run();
605
+ this.updateTableStats(tableName, 0, 0);
606
+
607
+ // Vacuum para recuperar espaço
608
+ db.exec('VACUUM');
609
+
610
+ return result.changes;
611
+ }
612
+
613
+ /**
614
+ * Conta total de itens na tabela
615
+ */
616
+ getTotalItems(tableName) {
617
+ const db = this.getDatabase(tableName);
618
+ const stmtCache = this.preparedStatements.get(tableName);
619
+ const stmt = stmtCache ? stmtCache.get('countItems') : db.prepare(`SELECT COUNT(*) as count FROM items`);
620
+
621
+ const row = stmt.get();
622
+ return row.count;
623
+ }
624
+
625
+ /**
626
+ * Calcula tamanho aproximado da tabela
627
+ */
628
+ getTableSize(tableName) {
629
+ const db = this.getDatabase(tableName);
630
+ const stmtCache = this.preparedStatements.get(tableName);
631
+ const stmt = stmtCache ? stmtCache.get('tableSize') : db.prepare(`SELECT SUM(LENGTH(data)) as size FROM items`);
632
+
633
+ const row = stmt.get();
634
+ return row.size || 0;
635
+ }
636
+
637
+ /**
638
+ * API compatível com LocalStore - Lê todos os itens
639
+ */
640
+ read(tableName) {
641
+ return this.scanTableOptimized(tableName);
642
+ }
643
+
644
+ /**
645
+ * API compatível com LocalStore - Escreve todos os itens (substitui)
646
+ */
647
+ write(tableName, items) {
648
+ this.truncateTable(tableName);
649
+
650
+ const table = this.getTable(tableName);
651
+ if (!table) return;
652
+
653
+ const batchOps = items.map(item => ({
654
+ type: 'put',
655
+ pkHash: String(item[table.hashKey]),
656
+ pkRange: table.rangeKey ? String(item[table.rangeKey]) : null,
657
+ item: item,
658
+ createdAt: item._createdAt,
659
+ updatedAt: item._updatedAt || new Date().toISOString()
660
+ }));
661
+
662
+ if (batchOps.length > 0) {
663
+ this.batchWrite(tableName, batchOps);
664
+ this.updateTableStats(tableName, items.length, JSON.stringify(items).length);
665
+ }
666
+ }
667
+
668
+ /**
669
+ * API compatível com LocalStore - Delete (por nome do arquivo)
670
+ */
671
+ delete(filePath) {
672
+ const tableName = path.basename(filePath, '.json');
673
+ if (this.exists(tableName)) {
674
+ this.deleteTable(tableName);
675
+ }
676
+ }
677
+
678
+ /**
679
+ * API compatível com LocalStore - Get file path
680
+ */
681
+ getFilePath(name) {
682
+ return path.join(this.dataDir, `${name}.db`);
683
+ }
684
+
685
+ /**
686
+ * Fecha todas as conexões
687
+ */
688
+ closeAll() {
689
+ for (const [tableName, db] of this.databases.entries()) {
690
+ db.close();
691
+ }
692
+ this.databases.clear();
693
+ this.preparedStatements.clear();
694
+ }
695
+
696
+ /**
697
+ * Fecha conexão específica
698
+ */
699
+ close(tableName) {
700
+ if (this.databases.has(tableName)) {
701
+ this.databases.get(tableName).close();
702
+ this.databases.delete(tableName);
703
+ this.preparedStatements.delete(tableName);
704
+ }
705
+ }
706
+
707
+ /**
708
+ * Obtém caminho do arquivo de metadados
709
+ */
710
+ getMetadataFilePath() {
711
+ return this.metadataFile;
712
+ }
713
+
714
+ /**
715
+ * Obtém todos os itens (alias para read)
716
+ */
717
+ getAllItems(tableName) {
718
+ return this.read(tableName);
719
+ }
720
+ }
721
+
722
+ module.exports = SQLiteStore;