s3db.js 11.3.2 → 12.0.0
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 +102 -8
- package/dist/s3db.cjs.js +36664 -15480
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.d.ts +57 -0
- package/dist/s3db.es.js +36661 -15531
- package/dist/s3db.es.js.map +1 -1
- package/mcp/entrypoint.js +58 -0
- package/mcp/tools/documentation.js +434 -0
- package/mcp/tools/index.js +4 -0
- package/package.json +27 -6
- package/src/behaviors/user-managed.js +13 -6
- package/src/client.class.js +41 -46
- package/src/concerns/base62.js +85 -0
- package/src/concerns/dictionary-encoding.js +294 -0
- package/src/concerns/geo-encoding.js +256 -0
- package/src/concerns/high-performance-inserter.js +34 -30
- package/src/concerns/ip.js +325 -0
- package/src/concerns/metadata-encoding.js +345 -66
- package/src/concerns/money.js +193 -0
- package/src/concerns/partition-queue.js +7 -4
- package/src/concerns/plugin-storage.js +39 -19
- package/src/database.class.js +76 -74
- package/src/errors.js +0 -4
- package/src/plugins/api/auth/api-key-auth.js +88 -0
- package/src/plugins/api/auth/basic-auth.js +154 -0
- package/src/plugins/api/auth/index.js +112 -0
- package/src/plugins/api/auth/jwt-auth.js +169 -0
- package/src/plugins/api/index.js +539 -0
- package/src/plugins/api/middlewares/index.js +15 -0
- package/src/plugins/api/middlewares/validator.js +185 -0
- package/src/plugins/api/routes/auth-routes.js +241 -0
- package/src/plugins/api/routes/resource-routes.js +304 -0
- package/src/plugins/api/server.js +350 -0
- package/src/plugins/api/utils/error-handler.js +147 -0
- package/src/plugins/api/utils/openapi-generator.js +1240 -0
- package/src/plugins/api/utils/response-formatter.js +218 -0
- package/src/plugins/backup/streaming-exporter.js +132 -0
- package/src/plugins/backup.plugin.js +103 -50
- package/src/plugins/cache/s3-cache.class.js +95 -47
- package/src/plugins/cache.plugin.js +107 -9
- package/src/plugins/concerns/plugin-dependencies.js +313 -0
- package/src/plugins/concerns/prometheus-formatter.js +255 -0
- package/src/plugins/consumers/rabbitmq-consumer.js +4 -0
- package/src/plugins/consumers/sqs-consumer.js +4 -0
- package/src/plugins/costs.plugin.js +255 -39
- package/src/plugins/eventual-consistency/helpers.js +15 -1
- package/src/plugins/geo.plugin.js +873 -0
- package/src/plugins/importer/index.js +1020 -0
- package/src/plugins/index.js +11 -0
- package/src/plugins/metrics.plugin.js +163 -4
- package/src/plugins/queue-consumer.plugin.js +6 -27
- package/src/plugins/relation.errors.js +139 -0
- package/src/plugins/relation.plugin.js +1242 -0
- package/src/plugins/replicators/bigquery-replicator.class.js +180 -8
- package/src/plugins/replicators/dynamodb-replicator.class.js +383 -0
- package/src/plugins/replicators/index.js +28 -3
- package/src/plugins/replicators/mongodb-replicator.class.js +391 -0
- package/src/plugins/replicators/mysql-replicator.class.js +558 -0
- package/src/plugins/replicators/planetscale-replicator.class.js +409 -0
- package/src/plugins/replicators/postgres-replicator.class.js +182 -7
- package/src/plugins/replicators/s3db-replicator.class.js +1 -12
- package/src/plugins/replicators/schema-sync.helper.js +601 -0
- package/src/plugins/replicators/sqs-replicator.class.js +11 -9
- package/src/plugins/replicators/turso-replicator.class.js +416 -0
- package/src/plugins/replicators/webhook-replicator.class.js +612 -0
- package/src/plugins/state-machine.plugin.js +122 -68
- package/src/plugins/tfstate/README.md +745 -0
- package/src/plugins/tfstate/base-driver.js +80 -0
- package/src/plugins/tfstate/errors.js +112 -0
- package/src/plugins/tfstate/filesystem-driver.js +129 -0
- package/src/plugins/tfstate/index.js +2660 -0
- package/src/plugins/tfstate/s3-driver.js +192 -0
- package/src/plugins/ttl.plugin.js +536 -0
- package/src/resource.class.js +14 -10
- package/src/s3db.d.ts +57 -0
- package/src/schema.class.js +366 -32
- package/SECURITY.md +0 -76
- package/src/partition-drivers/base-partition-driver.js +0 -106
- package/src/partition-drivers/index.js +0 -66
- package/src/partition-drivers/memory-partition-driver.js +0 -289
- package/src/partition-drivers/sqs-partition-driver.js +0 -337
- package/src/partition-drivers/sync-partition-driver.js +0 -38
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
# TfState Plugin - Internal Development Guide
|
|
2
|
+
|
|
3
|
+
Este documento é o guia interno de desenvolvimento do TfState Plugin.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 📋 Arquitetura Geral
|
|
8
|
+
|
|
9
|
+
### Filosofia
|
|
10
|
+
|
|
11
|
+
O TfState Plugin transforma states do Terraform (arquivos `.tfstate`) em dados consultáveis no s3db.
|
|
12
|
+
|
|
13
|
+
**Princípios:**
|
|
14
|
+
- ✅ **Simplicidade**: API clara e direta
|
|
15
|
+
- ✅ **Performance**: Partitions para queries rápidas (sync mode)
|
|
16
|
+
- ✅ **Flexibilidade**: Suporta local files, S3, glob patterns
|
|
17
|
+
- ✅ **Rastreabilidade**: Diff tracking entre versões
|
|
18
|
+
- ✅ **Deduplicação**: SHA256 hash para evitar re-imports
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 🗄️ Os 3 Resources
|
|
23
|
+
|
|
24
|
+
### 1. State Files Resource (`plg_tfstate_states`)
|
|
25
|
+
|
|
26
|
+
Armazena metadados sobre cada `.tfstate` importado.
|
|
27
|
+
|
|
28
|
+
**Schema Completo:**
|
|
29
|
+
```javascript
|
|
30
|
+
{
|
|
31
|
+
id: 'string|required', // nanoid gerado
|
|
32
|
+
sourceFile: 'string|required', // 'prod/terraform.tfstate'
|
|
33
|
+
serial: 'number|required', // Serial do state
|
|
34
|
+
lineage: 'string', // Terraform lineage
|
|
35
|
+
terraformVersion: 'string', // e.g. '1.5.0'
|
|
36
|
+
resourceCount: 'number', // Quantos recursos
|
|
37
|
+
sha256Hash: 'string|required', // Para dedup
|
|
38
|
+
importedAt: 'number|required', // timestamp
|
|
39
|
+
stateVersion: 'number' // 3 ou 4
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Partitions:**
|
|
44
|
+
```javascript
|
|
45
|
+
{
|
|
46
|
+
bySourceFile: { fields: { sourceFile: 'string' } },
|
|
47
|
+
bySerial: { fields: { serial: 'number' } }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
asyncPartitions: false // Sync para queries imediatas
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Queries Comuns:**
|
|
54
|
+
```javascript
|
|
55
|
+
// Buscar última versão de um state
|
|
56
|
+
const latest = await stateFilesResource.listPartition({
|
|
57
|
+
partition: 'bySourceFile',
|
|
58
|
+
partitionValues: { sourceFile: 'prod/terraform.tfstate' }
|
|
59
|
+
}).then(results => results.sort((a, b) => b.serial - a.serial)[0]);
|
|
60
|
+
|
|
61
|
+
// Buscar serial específico
|
|
62
|
+
const v100 = await stateFilesResource.listPartition({
|
|
63
|
+
partition: 'bySerial',
|
|
64
|
+
partitionValues: { serial: 100 }
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
### 2. Resources Resource (`plg_tfstate_resources`)
|
|
71
|
+
|
|
72
|
+
O resource principal contendo todos os recursos de infraestrutura extraídos dos states.
|
|
73
|
+
|
|
74
|
+
**Schema Completo:**
|
|
75
|
+
```javascript
|
|
76
|
+
{
|
|
77
|
+
id: 'string|required', // nanoid gerado
|
|
78
|
+
stateFileId: 'string|required', // FK para states resource
|
|
79
|
+
|
|
80
|
+
// Denormalized para queries
|
|
81
|
+
stateSerial: 'number|required', // De qual versão veio
|
|
82
|
+
sourceFile: 'string|required', // De qual arquivo veio
|
|
83
|
+
|
|
84
|
+
// Identidade do recurso
|
|
85
|
+
resourceType: 'string|required', // 'aws_instance'
|
|
86
|
+
resourceName: 'string|required', // 'web_server'
|
|
87
|
+
resourceAddress: 'string|required', // 'aws_instance.web_server'
|
|
88
|
+
providerName: 'string|required', // 'aws', 'google', 'azure', etc
|
|
89
|
+
|
|
90
|
+
// Dados do recurso
|
|
91
|
+
mode: 'string', // 'managed' ou 'data'
|
|
92
|
+
attributes: 'json', // Atributos completos do recurso
|
|
93
|
+
dependencies: 'array', // Lista de dependências
|
|
94
|
+
|
|
95
|
+
importedAt: 'number|required' // timestamp
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Partitions (crítico para performance!):**
|
|
100
|
+
```javascript
|
|
101
|
+
{
|
|
102
|
+
byType: {
|
|
103
|
+
fields: { resourceType: 'string' }
|
|
104
|
+
},
|
|
105
|
+
byProvider: {
|
|
106
|
+
fields: { providerName: 'string' }
|
|
107
|
+
},
|
|
108
|
+
bySerial: {
|
|
109
|
+
fields: { stateSerial: 'number' }
|
|
110
|
+
},
|
|
111
|
+
bySourceFile: {
|
|
112
|
+
fields: { sourceFile: 'string' }
|
|
113
|
+
},
|
|
114
|
+
byProviderAndType: {
|
|
115
|
+
fields: {
|
|
116
|
+
providerName: 'string',
|
|
117
|
+
resourceType: 'string'
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
asyncPartitions: false // IMPORTANTE: Sync para queries imediatas!
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Provider Detection Logic:**
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
function detectProvider(resourceType) {
|
|
129
|
+
const prefix = resourceType.split('_')[0];
|
|
130
|
+
|
|
131
|
+
const providerMap = {
|
|
132
|
+
'aws': 'aws',
|
|
133
|
+
'google': 'google',
|
|
134
|
+
'azurerm': 'azure',
|
|
135
|
+
'azuread': 'azure',
|
|
136
|
+
'kubernetes': 'kubernetes',
|
|
137
|
+
'helm': 'kubernetes',
|
|
138
|
+
'random': 'random',
|
|
139
|
+
'null': 'null',
|
|
140
|
+
'local': 'local',
|
|
141
|
+
'time': 'time',
|
|
142
|
+
'tls': 'tls'
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return providerMap[prefix] || 'unknown';
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Queries Comuns:**
|
|
150
|
+
```javascript
|
|
151
|
+
// Query por tipo (usa partition - O(1))
|
|
152
|
+
const ec2 = await resource.listPartition({
|
|
153
|
+
partition: 'byType',
|
|
154
|
+
partitionValues: { resourceType: 'aws_instance' }
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Query por provider (usa partition - O(1))
|
|
158
|
+
const awsResources = await resource.listPartition({
|
|
159
|
+
partition: 'byProvider',
|
|
160
|
+
partitionValues: { providerName: 'aws' }
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Query por provider + tipo (partition combinada - O(1))
|
|
164
|
+
const awsRds = await resource.listPartition({
|
|
165
|
+
partition: 'byProviderAndType',
|
|
166
|
+
partitionValues: {
|
|
167
|
+
providerName: 'aws',
|
|
168
|
+
resourceType: 'aws_db_instance'
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
### 3. Diffs Resource (`plg_tfstate_diffs`)
|
|
176
|
+
|
|
177
|
+
Rastreia mudanças entre versões de states.
|
|
178
|
+
|
|
179
|
+
**Schema Completo:**
|
|
180
|
+
```javascript
|
|
181
|
+
{
|
|
182
|
+
id: 'string|required', // nanoid gerado
|
|
183
|
+
sourceFile: 'string|required', // Qual state
|
|
184
|
+
oldSerial: 'number|required', // Versão antiga
|
|
185
|
+
newSerial: 'number|required', // Versão nova
|
|
186
|
+
|
|
187
|
+
summary: {
|
|
188
|
+
type: 'object',
|
|
189
|
+
props: {
|
|
190
|
+
addedCount: 'number', // Quantos adicionados
|
|
191
|
+
modifiedCount: 'number', // Quantos modificados
|
|
192
|
+
deletedCount: 'number' // Quantos deletados
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
changes: {
|
|
197
|
+
type: 'object',
|
|
198
|
+
props: {
|
|
199
|
+
added: 'array', // [{ type, name, address, attributes }]
|
|
200
|
+
modified: 'array', // [{ type, name, address, changes: [...] }]
|
|
201
|
+
deleted: 'array' // [{ type, name, address, attributes }]
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
calculatedAt: 'number|required' // timestamp
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Partitions:**
|
|
210
|
+
```javascript
|
|
211
|
+
{
|
|
212
|
+
bySourceFile: {
|
|
213
|
+
fields: { sourceFile: 'string' }
|
|
214
|
+
},
|
|
215
|
+
byOldSerial: {
|
|
216
|
+
fields: { oldSerial: 'number' }
|
|
217
|
+
},
|
|
218
|
+
byNewSerial: {
|
|
219
|
+
fields: { newSerial: 'number' }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
asyncPartitions: false // Sync para queries imediatas
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Diff Calculation Logic:**
|
|
227
|
+
|
|
228
|
+
```javascript
|
|
229
|
+
async function calculateDiff(oldState, newState) {
|
|
230
|
+
const oldResources = createResourceMap(oldState);
|
|
231
|
+
const newResources = createResourceMap(newState);
|
|
232
|
+
|
|
233
|
+
const added = [];
|
|
234
|
+
const deleted = [];
|
|
235
|
+
const modified = [];
|
|
236
|
+
|
|
237
|
+
// Detectar adicionados
|
|
238
|
+
for (const [address, resource] of Object.entries(newResources)) {
|
|
239
|
+
if (!oldResources[address]) {
|
|
240
|
+
added.push({
|
|
241
|
+
type: resource.type,
|
|
242
|
+
name: resource.name,
|
|
243
|
+
address: resource.address,
|
|
244
|
+
attributes: resource.attributes
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Detectar deletados
|
|
250
|
+
for (const [address, resource] of Object.entries(oldResources)) {
|
|
251
|
+
if (!newResources[address]) {
|
|
252
|
+
deleted.push({
|
|
253
|
+
type: resource.type,
|
|
254
|
+
name: resource.name,
|
|
255
|
+
address: resource.address,
|
|
256
|
+
attributes: resource.attributes
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Detectar modificados
|
|
262
|
+
for (const [address, newResource] of Object.entries(newResources)) {
|
|
263
|
+
const oldResource = oldResources[address];
|
|
264
|
+
if (oldResource) {
|
|
265
|
+
const changes = detectChanges(oldResource.attributes, newResource.attributes);
|
|
266
|
+
if (changes.length > 0) {
|
|
267
|
+
modified.push({
|
|
268
|
+
type: newResource.type,
|
|
269
|
+
name: newResource.name,
|
|
270
|
+
address: newResource.address,
|
|
271
|
+
changes: changes
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
summary: {
|
|
279
|
+
addedCount: added.length,
|
|
280
|
+
modifiedCount: modified.length,
|
|
281
|
+
deletedCount: deleted.length
|
|
282
|
+
},
|
|
283
|
+
changes: {
|
|
284
|
+
added,
|
|
285
|
+
modified,
|
|
286
|
+
deleted
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function detectChanges(oldAttrs, newAttrs, path = '') {
|
|
292
|
+
const changes = [];
|
|
293
|
+
|
|
294
|
+
// Comparar cada campo
|
|
295
|
+
const allKeys = new Set([...Object.keys(oldAttrs), ...Object.keys(newAttrs)]);
|
|
296
|
+
|
|
297
|
+
for (const key of allKeys) {
|
|
298
|
+
const fieldPath = path ? `${path}.${key}` : key;
|
|
299
|
+
const oldValue = oldAttrs[key];
|
|
300
|
+
const newValue = newAttrs[key];
|
|
301
|
+
|
|
302
|
+
if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
|
|
303
|
+
changes.push({
|
|
304
|
+
field: fieldPath,
|
|
305
|
+
oldValue: oldValue,
|
|
306
|
+
newValue: newValue
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return changes;
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## 🔧 Métodos Principais
|
|
318
|
+
|
|
319
|
+
### Import Flow
|
|
320
|
+
|
|
321
|
+
```
|
|
322
|
+
importState(filePath)
|
|
323
|
+
↓
|
|
324
|
+
1. Ler arquivo do filesystem
|
|
325
|
+
2. Parsear JSON
|
|
326
|
+
3. Calcular SHA256
|
|
327
|
+
4. Verificar se já existe (dedup)
|
|
328
|
+
5. Se novo:
|
|
329
|
+
- Criar record em stateFilesResource
|
|
330
|
+
- Extrair recursos
|
|
331
|
+
- Criar records em resource
|
|
332
|
+
- Se tem versão anterior:
|
|
333
|
+
- Calcular diff
|
|
334
|
+
- Criar record em diffsResource
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
**Código:**
|
|
338
|
+
```javascript
|
|
339
|
+
async importState(filePath, options = {}) {
|
|
340
|
+
// 1. Ler e parsear
|
|
341
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
342
|
+
const state = JSON.parse(content);
|
|
343
|
+
|
|
344
|
+
// 2. SHA256
|
|
345
|
+
const sha256Hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
346
|
+
|
|
347
|
+
// 3. Verificar se já existe
|
|
348
|
+
const existing = await this.stateFilesResource.query({ sha256Hash });
|
|
349
|
+
if (existing.length > 0) {
|
|
350
|
+
return { alreadyImported: true, stateFileId: existing[0].id };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 4. Criar state file record
|
|
354
|
+
const sourceFile = options.sourceFile || path.basename(filePath);
|
|
355
|
+
const stateFileRecord = await this.stateFilesResource.insert({
|
|
356
|
+
sourceFile,
|
|
357
|
+
serial: state.serial,
|
|
358
|
+
lineage: state.lineage,
|
|
359
|
+
terraformVersion: state.terraform_version,
|
|
360
|
+
resourceCount: state.resources?.length || 0,
|
|
361
|
+
sha256Hash,
|
|
362
|
+
importedAt: Date.now(),
|
|
363
|
+
stateVersion: state.version
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// 5. Extrair e inserir recursos
|
|
367
|
+
const extractedResources = await this._extractResources(state, stateFileRecord.id);
|
|
368
|
+
|
|
369
|
+
// 6. Calcular diff se houver versão anterior
|
|
370
|
+
if (this.trackDiffs) {
|
|
371
|
+
await this._maybeCalculateDiff(sourceFile, state.serial);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
stateFileId: stateFileRecord.id,
|
|
376
|
+
resourcesExtracted: extractedResources.length
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Resource Extraction
|
|
382
|
+
|
|
383
|
+
```javascript
|
|
384
|
+
async _extractResources(state, stateFileId) {
|
|
385
|
+
const resources = state.resources || [];
|
|
386
|
+
const extracted = [];
|
|
387
|
+
|
|
388
|
+
for (const resource of resources) {
|
|
389
|
+
// Aplicar filtros
|
|
390
|
+
if (!this._shouldIncludeResource(resource)) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Processar cada instance do recurso
|
|
395
|
+
for (const instance of resource.instances || []) {
|
|
396
|
+
const providerName = this._detectProvider(resource.type);
|
|
397
|
+
|
|
398
|
+
const record = {
|
|
399
|
+
stateFileId,
|
|
400
|
+
stateSerial: state.serial,
|
|
401
|
+
sourceFile: stateFileRecord.sourceFile,
|
|
402
|
+
resourceType: resource.type,
|
|
403
|
+
resourceName: resource.name,
|
|
404
|
+
resourceAddress: `${resource.type}.${resource.name}`,
|
|
405
|
+
providerName,
|
|
406
|
+
mode: resource.mode || 'managed',
|
|
407
|
+
attributes: instance.attributes || {},
|
|
408
|
+
dependencies: resource.depends_on || [],
|
|
409
|
+
importedAt: Date.now()
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
await this.resource.insert(record);
|
|
413
|
+
extracted.push(record);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return extracted;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
_shouldIncludeResource(resource) {
|
|
421
|
+
// Filtro por tipo
|
|
422
|
+
if (this.filters?.types && this.filters.types.length > 0) {
|
|
423
|
+
if (!this.filters.types.includes(resource.type)) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Filtro por provider
|
|
429
|
+
if (this.filters?.providers && this.filters.providers.length > 0) {
|
|
430
|
+
const provider = this._detectProvider(resource.type);
|
|
431
|
+
if (!this.filters.providers.includes(provider)) {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Filtro de exclusão
|
|
437
|
+
if (this.filters?.exclude && this.filters.exclude.length > 0) {
|
|
438
|
+
for (const pattern of this.filters.exclude) {
|
|
439
|
+
if (this._matchesPattern(resource.type, pattern)) {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
_detectProvider(resourceType) {
|
|
449
|
+
const prefix = resourceType.split('_')[0];
|
|
450
|
+
|
|
451
|
+
const providerMap = {
|
|
452
|
+
'aws': 'aws',
|
|
453
|
+
'google': 'google',
|
|
454
|
+
'azurerm': 'azure',
|
|
455
|
+
'azuread': 'azure',
|
|
456
|
+
'kubernetes': 'kubernetes',
|
|
457
|
+
'helm': 'kubernetes',
|
|
458
|
+
'random': 'random',
|
|
459
|
+
'null': 'null',
|
|
460
|
+
'local': 'local',
|
|
461
|
+
'time': 'time',
|
|
462
|
+
'tls': 'tls'
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
return providerMap[prefix] || 'unknown';
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Diff Calculation
|
|
470
|
+
|
|
471
|
+
```javascript
|
|
472
|
+
async _maybeCalculateDiff(sourceFile, newSerial) {
|
|
473
|
+
// Buscar versão anterior
|
|
474
|
+
const previousStates = await this.stateFilesResource.listPartition({
|
|
475
|
+
partition: 'bySourceFile',
|
|
476
|
+
partitionValues: { sourceFile }
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
if (previousStates.length < 2) {
|
|
480
|
+
return; // Primeira versão, sem diff
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Ordenar por serial
|
|
484
|
+
previousStates.sort((a, b) => b.serial - a.serial);
|
|
485
|
+
|
|
486
|
+
const newState = previousStates[0];
|
|
487
|
+
const oldState = previousStates[1];
|
|
488
|
+
|
|
489
|
+
if (newState.serial === newSerial) {
|
|
490
|
+
// Buscar recursos de ambas as versões
|
|
491
|
+
const newResources = await this.resource.listPartition({
|
|
492
|
+
partition: 'bySerial',
|
|
493
|
+
partitionValues: { stateSerial: newState.serial }
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const oldResources = await this.resource.listPartition({
|
|
497
|
+
partition: 'bySerial',
|
|
498
|
+
partitionValues: { stateSerial: oldState.serial }
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// Calcular diff
|
|
502
|
+
const diff = this._calculateDiff(oldResources, newResources);
|
|
503
|
+
|
|
504
|
+
// Salvar diff
|
|
505
|
+
await this.diffsResource.insert({
|
|
506
|
+
sourceFile,
|
|
507
|
+
oldSerial: oldState.serial,
|
|
508
|
+
newSerial: newState.serial,
|
|
509
|
+
summary: diff.summary,
|
|
510
|
+
changes: diff.changes,
|
|
511
|
+
calculatedAt: Date.now()
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## 🎯 Query Helpers
|
|
520
|
+
|
|
521
|
+
Métodos convenientes que usam partitions para queries rápidas:
|
|
522
|
+
|
|
523
|
+
```javascript
|
|
524
|
+
async getResourcesByType(type) {
|
|
525
|
+
return this.resource.listPartition({
|
|
526
|
+
partition: 'byType',
|
|
527
|
+
partitionValues: { resourceType: type }
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async getResourcesByProvider(provider) {
|
|
532
|
+
return this.resource.listPartition({
|
|
533
|
+
partition: 'byProvider',
|
|
534
|
+
partitionValues: { providerName: provider }
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async getResourcesByProviderAndType(provider, type) {
|
|
539
|
+
return this.resource.listPartition({
|
|
540
|
+
partition: 'byProviderAndType',
|
|
541
|
+
partitionValues: {
|
|
542
|
+
providerName: provider,
|
|
543
|
+
resourceType: type
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async getDiff(sourceFile, oldSerial, newSerial) {
|
|
549
|
+
const diffs = await this.diffsResource.query({
|
|
550
|
+
sourceFile,
|
|
551
|
+
oldSerial,
|
|
552
|
+
newSerial
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
return diffs[0] || null;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async getLatestDiff(sourceFile) {
|
|
559
|
+
const diffs = await this.diffsResource.listPartition({
|
|
560
|
+
partition: 'bySourceFile',
|
|
561
|
+
partitionValues: { sourceFile }
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
if (diffs.length === 0) return null;
|
|
565
|
+
|
|
566
|
+
// Ordenar por calculatedAt desc
|
|
567
|
+
diffs.sort((a, b) => b.calculatedAt - a.calculatedAt);
|
|
568
|
+
return diffs[0];
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async getAllDiffs(sourceFile) {
|
|
572
|
+
return this.diffsResource.listPartition({
|
|
573
|
+
partition: 'bySourceFile',
|
|
574
|
+
partitionValues: { sourceFile }
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
## 📊 Statistics
|
|
582
|
+
|
|
583
|
+
```javascript
|
|
584
|
+
async getStats() {
|
|
585
|
+
const states = await this.stateFilesResource.list();
|
|
586
|
+
const resources = await this.resource.list();
|
|
587
|
+
const diffs = await this.diffsResource.list();
|
|
588
|
+
|
|
589
|
+
// Group by provider
|
|
590
|
+
const providers = {};
|
|
591
|
+
resources.forEach(r => {
|
|
592
|
+
providers[r.providerName] = (providers[r.providerName] || 0) + 1;
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// Group by type
|
|
596
|
+
const types = {};
|
|
597
|
+
resources.forEach(r => {
|
|
598
|
+
types[r.resourceType] = (types[r.resourceType] || 0) + 1;
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// Latest serial
|
|
602
|
+
const latestSerial = states.length > 0
|
|
603
|
+
? Math.max(...states.map(s => s.serial))
|
|
604
|
+
: 0;
|
|
605
|
+
|
|
606
|
+
return {
|
|
607
|
+
totalStates: states.length,
|
|
608
|
+
totalResources: resources.length,
|
|
609
|
+
totalDiffs: diffs.length,
|
|
610
|
+
latestSerial,
|
|
611
|
+
providers,
|
|
612
|
+
types
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async getStatsByProvider() {
|
|
617
|
+
const resources = await this.resource.list();
|
|
618
|
+
|
|
619
|
+
const stats = {};
|
|
620
|
+
resources.forEach(r => {
|
|
621
|
+
stats[r.providerName] = (stats[r.providerName] || 0) + 1;
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
return stats;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async getStatsByType() {
|
|
628
|
+
const resources = await this.resource.list();
|
|
629
|
+
|
|
630
|
+
const stats = {};
|
|
631
|
+
resources.forEach(r => {
|
|
632
|
+
stats[r.resourceType] = (stats[r.resourceType] || 0) + 1;
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
return stats;
|
|
636
|
+
}
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## ⚡ Performance Considerations
|
|
642
|
+
|
|
643
|
+
### 1. Partitions em Sync Mode
|
|
644
|
+
|
|
645
|
+
**CRÍTICO**: Todas as 3 resources usam `asyncPartitions: false`.
|
|
646
|
+
|
|
647
|
+
**Por quê?**
|
|
648
|
+
- Queries precisam ser imediatas após o import
|
|
649
|
+
- Partitions async criam race conditions
|
|
650
|
+
- Diff tracking requer dados imediatos
|
|
651
|
+
|
|
652
|
+
**Trade-off:**
|
|
653
|
+
- Insert é um pouco mais lento (mas ainda rápido)
|
|
654
|
+
- Queries são O(1) usando partitions
|
|
655
|
+
|
|
656
|
+
### 2. Denormalização
|
|
657
|
+
|
|
658
|
+
Os campos `stateSerial` e `sourceFile` são denormalizados no resources resource para permitir queries rápidas sem joins.
|
|
659
|
+
|
|
660
|
+
### 3. SHA256 Deduplication
|
|
661
|
+
|
|
662
|
+
Antes de importar, sempre verificamos se o SHA256 já existe. Isso evita re-imports desnecessários.
|
|
663
|
+
|
|
664
|
+
### 4. Batch Operations
|
|
665
|
+
|
|
666
|
+
Para glob imports, processamos em paralelo mas com limite:
|
|
667
|
+
|
|
668
|
+
```javascript
|
|
669
|
+
const concurrency = 5; // Max 5 imports simultâneos
|
|
670
|
+
await PromisePool
|
|
671
|
+
.withConcurrency(concurrency)
|
|
672
|
+
.for(files)
|
|
673
|
+
.process(async file => await this.importState(file));
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
---
|
|
677
|
+
|
|
678
|
+
## 🧪 Testing Strategy
|
|
679
|
+
|
|
680
|
+
### 1. Unit Tests
|
|
681
|
+
|
|
682
|
+
Testar métodos isolados:
|
|
683
|
+
- `_detectProvider()` → Detecção correta de providers
|
|
684
|
+
- `_shouldIncludeResource()` → Filtros funcionando
|
|
685
|
+
- `_calculateDiff()` → Diff calculation correto
|
|
686
|
+
|
|
687
|
+
### 2. Integration Tests
|
|
688
|
+
|
|
689
|
+
Testar fluxos completos:
|
|
690
|
+
- Import → Verificar resources criados
|
|
691
|
+
- Import 2x → Verificar dedup funciona
|
|
692
|
+
- Import v1 + v2 → Verificar diff criado
|
|
693
|
+
|
|
694
|
+
### 3. Partition Tests
|
|
695
|
+
|
|
696
|
+
Testar queries usando partitions:
|
|
697
|
+
- `getResourcesByType()` → Deve usar partition
|
|
698
|
+
- `getResourcesByProvider()` → Deve usar partition
|
|
699
|
+
- `getResourcesByProviderAndType()` → Deve usar partition combinada
|
|
700
|
+
|
|
701
|
+
### 4. Performance Tests
|
|
702
|
+
|
|
703
|
+
Verificar que partitions são rápidas:
|
|
704
|
+
- Import 1000 resources
|
|
705
|
+
- Query por tipo → Deve ser < 100ms
|
|
706
|
+
|
|
707
|
+
---
|
|
708
|
+
|
|
709
|
+
## 🐛 Common Issues
|
|
710
|
+
|
|
711
|
+
### Issue: Partitions retornam vazio
|
|
712
|
+
|
|
713
|
+
**Causa**: `asyncPartitions: true` (default)
|
|
714
|
+
|
|
715
|
+
**Solução**: Sempre usar `asyncPartitions: false` nos 3 resources
|
|
716
|
+
|
|
717
|
+
### Issue: Diff não está sendo criado
|
|
718
|
+
|
|
719
|
+
**Causa**: `trackDiffs: false` ou primeira versão do state
|
|
720
|
+
|
|
721
|
+
**Solução**: Verificar que `trackDiffs: true` e que há pelo menos 2 versões do state
|
|
722
|
+
|
|
723
|
+
### Issue: Provider detection errado
|
|
724
|
+
|
|
725
|
+
**Causa**: Provider não está no `providerMap`
|
|
726
|
+
|
|
727
|
+
**Solução**: Adicionar provider ao map em `_detectProvider()`
|
|
728
|
+
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
## 🚀 Future Enhancements
|
|
732
|
+
|
|
733
|
+
1. **Partial imports**: Importar apenas recursos modificados
|
|
734
|
+
2. **Compression**: Comprimir `attributes` JSON para economizar espaço
|
|
735
|
+
3. **Resource relationships**: Mapear dependências entre recursos
|
|
736
|
+
4. **Cost estimation**: Integrar com pricing APIs
|
|
737
|
+
5. **Compliance checks**: Validar recursos contra políticas
|
|
738
|
+
|
|
739
|
+
---
|
|
740
|
+
|
|
741
|
+
## 📚 References
|
|
742
|
+
|
|
743
|
+
- [Terraform State Format](https://www.terraform.io/internals/json-format)
|
|
744
|
+
- [s3db Partitioning Guide](../../docs/partitioning.md)
|
|
745
|
+
- [Plugin Development](../../docs/plugins.md)
|