@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.
- package/README.md +834 -834
- package/aws-config +153 -153
- package/bin/aws-local-simulator.js +63 -63
- package/package.json +3 -2
- package/src/config/config-loader.js +114 -114
- package/src/config/default-config.js +79 -79
- package/src/config/env-loader.js +68 -68
- package/src/index.js +146 -146
- package/src/index.mjs +123 -123
- package/src/server.js +463 -463
- package/src/services/apigateway/index.js +75 -75
- package/src/services/apigateway/server.js +607 -607
- package/src/services/apigateway/simulator.js +1405 -1405
- package/src/services/athena/index.js +75 -75
- package/src/services/athena/server.js +101 -101
- package/src/services/athena/simulador.js +998 -998
- package/src/services/athena/simulator.js +346 -346
- package/src/services/cloudformation/index.js +106 -106
- package/src/services/cloudformation/server.js +417 -417
- package/src/services/cloudformation/simulador.js +1020 -1020
- package/src/services/cloudtrail/index.js +84 -84
- package/src/services/cloudtrail/server.js +235 -235
- package/src/services/cloudtrail/simulador.js +719 -719
- package/src/services/cloudwatch/index.js +84 -84
- package/src/services/cloudwatch/server.js +366 -366
- package/src/services/cloudwatch/simulador.js +1173 -1173
- package/src/services/cognito/index.js +79 -79
- package/src/services/cognito/server.js +297 -297
- package/src/services/cognito/simulator.js +1992 -1761
- package/src/services/config/index.js +96 -96
- package/src/services/config/server.js +215 -215
- package/src/services/config/simulador.js +1260 -1260
- package/src/services/dynamodb/index.js +74 -74
- package/src/services/dynamodb/server.js +139 -139
- package/src/services/dynamodb/simulator.js +1005 -982
- package/src/services/dynamodb/sqlite-store.js +722 -0
- package/src/services/ecs/index.js +65 -65
- package/src/services/ecs/server.js +235 -235
- package/src/services/ecs/simulator.js +844 -844
- package/src/services/eventbridge/index.js +89 -89
- package/src/services/eventbridge/server.js +209 -209
- package/src/services/eventbridge/simulator.js +684 -684
- package/src/services/index.js +45 -45
- package/src/services/kms/index.js +75 -75
- package/src/services/kms/server.js +81 -81
- package/src/services/kms/simulator.js +344 -344
- package/src/services/lambda/handler-loader.js +183 -183
- package/src/services/lambda/index.js +81 -81
- package/src/services/lambda/route-registry.js +274 -274
- package/src/services/lambda/server.js +191 -191
- package/src/services/lambda/simulator.js +364 -364
- package/src/services/parameter-store/index.js +80 -80
- package/src/services/parameter-store/server.js +50 -50
- package/src/services/parameter-store/simulator.js +201 -201
- package/src/services/s3/index.js +73 -73
- package/src/services/s3/server.js +350 -350
- package/src/services/s3/simulator.js +568 -568
- package/src/services/secret-manager/index.js +80 -80
- package/src/services/secret-manager/server.js +51 -51
- package/src/services/secret-manager/simulator.js +182 -182
- package/src/services/sns/index.js +89 -89
- package/src/services/sns/server.js +607 -607
- package/src/services/sns/simulator.js +1482 -1482
- package/src/services/sqs/index.js +98 -98
- package/src/services/sqs/server.js +360 -360
- package/src/services/sqs/simulator.js +509 -509
- package/src/services/sts/index.js +37 -37
- package/src/services/sts/server.js +144 -144
- package/src/services/sts/simulator.js +69 -69
- package/src/services/xray/index.js +83 -83
- package/src/services/xray/server.js +308 -308
- package/src/services/xray/simulador.js +994 -994
- package/src/template/aws-config-template.js +87 -87
- package/src/template/aws-config-template.mjs +90 -90
- package/src/template/config-template.json +203 -203
- package/src/utils/aws-config.js +91 -91
- package/src/utils/cloudtrail-audit.js +129 -129
- package/src/utils/local-store.js +83 -83
- 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;
|