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.
Files changed (82) hide show
  1. package/README.md +102 -8
  2. package/dist/s3db.cjs.js +36664 -15480
  3. package/dist/s3db.cjs.js.map +1 -1
  4. package/dist/s3db.d.ts +57 -0
  5. package/dist/s3db.es.js +36661 -15531
  6. package/dist/s3db.es.js.map +1 -1
  7. package/mcp/entrypoint.js +58 -0
  8. package/mcp/tools/documentation.js +434 -0
  9. package/mcp/tools/index.js +4 -0
  10. package/package.json +27 -6
  11. package/src/behaviors/user-managed.js +13 -6
  12. package/src/client.class.js +41 -46
  13. package/src/concerns/base62.js +85 -0
  14. package/src/concerns/dictionary-encoding.js +294 -0
  15. package/src/concerns/geo-encoding.js +256 -0
  16. package/src/concerns/high-performance-inserter.js +34 -30
  17. package/src/concerns/ip.js +325 -0
  18. package/src/concerns/metadata-encoding.js +345 -66
  19. package/src/concerns/money.js +193 -0
  20. package/src/concerns/partition-queue.js +7 -4
  21. package/src/concerns/plugin-storage.js +39 -19
  22. package/src/database.class.js +76 -74
  23. package/src/errors.js +0 -4
  24. package/src/plugins/api/auth/api-key-auth.js +88 -0
  25. package/src/plugins/api/auth/basic-auth.js +154 -0
  26. package/src/plugins/api/auth/index.js +112 -0
  27. package/src/plugins/api/auth/jwt-auth.js +169 -0
  28. package/src/plugins/api/index.js +539 -0
  29. package/src/plugins/api/middlewares/index.js +15 -0
  30. package/src/plugins/api/middlewares/validator.js +185 -0
  31. package/src/plugins/api/routes/auth-routes.js +241 -0
  32. package/src/plugins/api/routes/resource-routes.js +304 -0
  33. package/src/plugins/api/server.js +350 -0
  34. package/src/plugins/api/utils/error-handler.js +147 -0
  35. package/src/plugins/api/utils/openapi-generator.js +1240 -0
  36. package/src/plugins/api/utils/response-formatter.js +218 -0
  37. package/src/plugins/backup/streaming-exporter.js +132 -0
  38. package/src/plugins/backup.plugin.js +103 -50
  39. package/src/plugins/cache/s3-cache.class.js +95 -47
  40. package/src/plugins/cache.plugin.js +107 -9
  41. package/src/plugins/concerns/plugin-dependencies.js +313 -0
  42. package/src/plugins/concerns/prometheus-formatter.js +255 -0
  43. package/src/plugins/consumers/rabbitmq-consumer.js +4 -0
  44. package/src/plugins/consumers/sqs-consumer.js +4 -0
  45. package/src/plugins/costs.plugin.js +255 -39
  46. package/src/plugins/eventual-consistency/helpers.js +15 -1
  47. package/src/plugins/geo.plugin.js +873 -0
  48. package/src/plugins/importer/index.js +1020 -0
  49. package/src/plugins/index.js +11 -0
  50. package/src/plugins/metrics.plugin.js +163 -4
  51. package/src/plugins/queue-consumer.plugin.js +6 -27
  52. package/src/plugins/relation.errors.js +139 -0
  53. package/src/plugins/relation.plugin.js +1242 -0
  54. package/src/plugins/replicators/bigquery-replicator.class.js +180 -8
  55. package/src/plugins/replicators/dynamodb-replicator.class.js +383 -0
  56. package/src/plugins/replicators/index.js +28 -3
  57. package/src/plugins/replicators/mongodb-replicator.class.js +391 -0
  58. package/src/plugins/replicators/mysql-replicator.class.js +558 -0
  59. package/src/plugins/replicators/planetscale-replicator.class.js +409 -0
  60. package/src/plugins/replicators/postgres-replicator.class.js +182 -7
  61. package/src/plugins/replicators/s3db-replicator.class.js +1 -12
  62. package/src/plugins/replicators/schema-sync.helper.js +601 -0
  63. package/src/plugins/replicators/sqs-replicator.class.js +11 -9
  64. package/src/plugins/replicators/turso-replicator.class.js +416 -0
  65. package/src/plugins/replicators/webhook-replicator.class.js +612 -0
  66. package/src/plugins/state-machine.plugin.js +122 -68
  67. package/src/plugins/tfstate/README.md +745 -0
  68. package/src/plugins/tfstate/base-driver.js +80 -0
  69. package/src/plugins/tfstate/errors.js +112 -0
  70. package/src/plugins/tfstate/filesystem-driver.js +129 -0
  71. package/src/plugins/tfstate/index.js +2660 -0
  72. package/src/plugins/tfstate/s3-driver.js +192 -0
  73. package/src/plugins/ttl.plugin.js +536 -0
  74. package/src/resource.class.js +14 -10
  75. package/src/s3db.d.ts +57 -0
  76. package/src/schema.class.js +366 -32
  77. package/SECURITY.md +0 -76
  78. package/src/partition-drivers/base-partition-driver.js +0 -106
  79. package/src/partition-drivers/index.js +0 -66
  80. package/src/partition-drivers/memory-partition-driver.js +0 -289
  81. package/src/partition-drivers/sqs-partition-driver.js +0 -337
  82. 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)