smoonb 0.0.12 → 0.0.14

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/commands/backup.js +411 -108
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoonb",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "description": "Complete Supabase backup and migration tool - EXPERIMENTAL VERSION - USE AT YOUR OWN RISK",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -5,8 +5,8 @@ const { ensureBin, runCommand } = require('../utils/cli');
5
5
  const { ensureDir, writeJson, copyDir } = require('../utils/fsx');
6
6
  const { sha256 } = require('../utils/hash');
7
7
  const { readConfig, validateFor } = require('../utils/config');
8
- const { IntrospectionService } = require('../services/introspect');
9
8
  const { showBetaBanner } = require('../utils/banner');
9
+ const { createClient } = require('@supabase/supabase-js');
10
10
 
11
11
  // Exportar FUNÇÃO em vez de objeto Command
12
12
  module.exports = async (options) => {
@@ -36,17 +36,24 @@ module.exports = async (options) => {
36
36
  // Resolver diretório de saída
37
37
  const outputDir = options.output || config.backup.outputDir;
38
38
 
39
- // Criar diretório de backup com timestamp
40
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
41
- const backupDir = path.join(outputDir, `backup-${timestamp}`);
39
+ // Criar diretório de backup com timestamp humanizado
40
+ const now = new Date();
41
+ const year = now.getFullYear();
42
+ const month = String(now.getMonth() + 1).padStart(2, '0');
43
+ const day = String(now.getDate()).padStart(2, '0');
44
+ const hour = String(now.getHours()).padStart(2, '0');
45
+ const minute = String(now.getMinutes()).padStart(2, '0');
46
+ const second = String(now.getSeconds()).padStart(2, '0');
47
+
48
+ const backupDir = path.join(outputDir, `backup-${year}-${month}-${day}-${hour}-${minute}-${second}`);
42
49
  await ensureDir(backupDir);
43
50
 
44
- console.log(chalk.blue(`🚀 Iniciando backup do projeto: ${config.supabase.projectId}`));
51
+ console.log(chalk.blue(`🚀 Iniciando backup COMPLETO do projeto: ${config.supabase.projectId}`));
45
52
  console.log(chalk.blue(`📁 Diretório: ${backupDir}`));
46
53
  console.log(chalk.gray(`🔧 Usando pg_dump: ${pgDumpPath}`));
47
54
 
48
- // 1. Backup da Database usando pg_dump/pg_dumpall
49
- console.log(chalk.blue('\n📊 1/3 - Backup da Database PostgreSQL...'));
55
+ // 1. Backup da Database PostgreSQL (básico)
56
+ console.log(chalk.blue('\n📊 1/6 - Backup da Database PostgreSQL...'));
50
57
  const dbBackupResult = await backupDatabaseWithPgDump(databaseUrl, backupDir, pgDumpPath);
51
58
 
52
59
  if (!dbBackupResult.success) {
@@ -58,20 +65,53 @@ module.exports = async (options) => {
58
65
  process.exit(1);
59
66
  }
60
67
 
61
- // 2. Gerar inventário real
62
- console.log(chalk.blue('\n🔍 2/3 - Gerando inventário completo...'));
63
- await generateInventory(config, backupDir);
64
-
65
- // 3. Backup das Edge Functions locais
66
- console.log(chalk.blue('\n 3/3 - Backup das Edge Functions locais...'));
67
- await backupLocalFunctions(backupDir);
68
-
69
- // Gerar manifesto do backup
70
- await generateBackupManifest(config, backupDir, dbBackupResult.files);
71
-
72
- console.log(chalk.green('\n🎉 Backup completo finalizado!'));
68
+ // 2. Backup das Edge Functions via Supabase API
69
+ console.log(chalk.blue('\n 2/6 - Backup das Edge Functions via API...'));
70
+ const edgeFunctionsResult = await backupEdgeFunctions(config, backupDir);
71
+
72
+ // 3. Backup das Auth Settings via Management API
73
+ console.log(chalk.blue('\n🔐 3/6 - Backup das Auth Settings via API...'));
74
+ const authSettingsResult = await backupAuthSettings(config, backupDir);
75
+
76
+ // 4. Backup do Storage via Supabase API
77
+ console.log(chalk.blue('\n📦 4/6 - Backup do Storage via API...'));
78
+ const storageResult = await backupStorage(config, backupDir);
79
+
80
+ // 5. Backup dos Custom Roles via SQL
81
+ console.log(chalk.blue('\n👥 5/6 - Backup dos Custom Roles via SQL...'));
82
+ const customRolesResult = await backupCustomRoles(databaseUrl, backupDir);
83
+
84
+ // 6. Backup das Realtime Settings via SQL
85
+ console.log(chalk.blue('\n🔄 6/6 - Backup das Realtime Settings via SQL...'));
86
+ const realtimeResult = await backupRealtimeSettings(databaseUrl, backupDir);
87
+
88
+ // Gerar manifesto do backup completo
89
+ await generateCompleteBackupManifest(config, backupDir, {
90
+ database: dbBackupResult,
91
+ edgeFunctions: edgeFunctionsResult,
92
+ authSettings: authSettingsResult,
93
+ storage: storageResult,
94
+ customRoles: customRolesResult,
95
+ realtime: realtimeResult
96
+ });
97
+
98
+ console.log(chalk.green('\n🎉 BACKUP COMPLETO FINALIZADO!'));
73
99
  console.log(chalk.blue(`📁 Localização: ${backupDir}`));
74
100
  console.log(chalk.green(`✅ Database: ${dbBackupResult.files.length} arquivos SQL gerados`));
101
+ console.log(chalk.green(`✅ Edge Functions: ${edgeFunctionsResult.functions.length} functions baixadas`));
102
+ console.log(chalk.green(`✅ Auth Settings: ${authSettingsResult.success ? 'Exportadas' : 'Falharam'}`));
103
+ console.log(chalk.green(`✅ Storage: ${storageResult.buckets.length} buckets verificados`));
104
+ console.log(chalk.green(`✅ Custom Roles: ${customRolesResult.roles.length} roles exportados`));
105
+ console.log(chalk.green(`✅ Realtime: ${realtimeResult.success ? 'Configurações exportadas' : 'Falharam'}`));
106
+
107
+ // Mostrar resumo dos arquivos
108
+ console.log(chalk.blue('\n📊 Resumo dos arquivos gerados:'));
109
+ for (const file of dbBackupResult.files) {
110
+ console.log(chalk.gray(` - ${file.filename}: ${file.sizeKB} KB`));
111
+ }
112
+ if (edgeFunctionsResult.functions.length > 0) {
113
+ console.log(chalk.gray(` - Edge Functions: ${edgeFunctionsResult.functions.length} functions`));
114
+ }
75
115
 
76
116
  } catch (error) {
77
117
  console.error(chalk.red(`❌ Erro no backup: ${error.message}`));
@@ -125,69 +165,85 @@ async function backupDatabaseWithPgDump(databaseUrl, backupDir, pgDumpPath) {
125
165
  const files = [];
126
166
  let success = true;
127
167
 
128
- // 1. Backup dos roles usando pg_dumpall
129
- console.log(chalk.blue(' - Exportando roles...'));
130
- const rolesFile = path.join(backupDir, 'roles.sql');
131
- const rolesCommand = `"${pgDumpPath.replace('pg_dump', 'pg_dumpall')}" --host=${host} --port=${port} --username=${username} --roles-only -f "${rolesFile}"`;
168
+ // 1. Backup do schema usando pg_dump
169
+ console.log(chalk.blue(' - Exportando schema...'));
170
+ const schemaFile = path.join(backupDir, 'schema.sql');
171
+ const schemaCommand = `"${pgDumpPath}" "${databaseUrl}" --schema-only -f "${schemaFile}"`;
132
172
 
133
173
  try {
134
- await runCommand(rolesCommand, {
174
+ await runCommand(schemaCommand, {
135
175
  env: { ...process.env, PGPASSWORD: password }
136
176
  });
137
177
 
138
- if (await validateSqlFile(rolesFile)) {
139
- files.push('roles.sql');
140
- console.log(chalk.green(' ✅ Roles exportados com sucesso'));
178
+ const schemaValidation = await validateSqlFile(schemaFile);
179
+ if (schemaValidation.valid) {
180
+ files.push({
181
+ filename: 'schema.sql',
182
+ size: schemaValidation.size,
183
+ sizeKB: schemaValidation.sizeKB
184
+ });
185
+ console.log(chalk.green(` ✅ Schema exportado: ${schemaValidation.sizeKB} KB`));
141
186
  } else {
142
- console.log(chalk.yellow(' ⚠️ Arquivo roles.sql está vazio'));
187
+ console.log(chalk.red(` Arquivo schema.sql inválido: ${schemaValidation.error}`));
143
188
  success = false;
144
189
  }
145
190
  } catch (error) {
146
- console.log(chalk.red(` ❌ Erro ao exportar roles: ${error.message}`));
191
+ console.log(chalk.red(` ❌ Erro ao exportar schema: ${error.message}`));
147
192
  success = false;
148
193
  }
149
194
 
150
- // 2. Backup do schema usando pg_dump
151
- console.log(chalk.blue(' - Exportando schema...'));
152
- const schemaFile = path.join(backupDir, 'schema.sql');
153
- const schemaCommand = `"${pgDumpPath}" --host=${host} --port=${port} --username=${username} --schema-only -f "${schemaFile}" ${database}`;
195
+ // 2. Backup dos dados usando pg_dump
196
+ console.log(chalk.blue(' - Exportando dados...'));
197
+ const dataFile = path.join(backupDir, 'data.sql');
198
+ const dataCommand = `"${pgDumpPath}" "${databaseUrl}" --data-only -f "${dataFile}"`;
154
199
 
155
200
  try {
156
- await runCommand(schemaCommand, {
201
+ await runCommand(dataCommand, {
157
202
  env: { ...process.env, PGPASSWORD: password }
158
203
  });
159
204
 
160
- if (await validateSqlFile(schemaFile)) {
161
- files.push('schema.sql');
162
- console.log(chalk.green(' ✅ Schema exportado com sucesso'));
205
+ const dataValidation = await validateSqlFile(dataFile);
206
+ if (dataValidation.valid) {
207
+ files.push({
208
+ filename: 'data.sql',
209
+ size: dataValidation.size,
210
+ sizeKB: dataValidation.sizeKB
211
+ });
212
+ console.log(chalk.green(` ✅ Dados exportados: ${dataValidation.sizeKB} KB`));
163
213
  } else {
164
- console.log(chalk.yellow(' ⚠️ Arquivo schema.sql está vazio'));
214
+ console.log(chalk.red(` Arquivo data.sql inválido: ${dataValidation.error}`));
165
215
  success = false;
166
216
  }
167
217
  } catch (error) {
168
- console.log(chalk.red(` ❌ Erro ao exportar schema: ${error.message}`));
218
+ console.log(chalk.red(` ❌ Erro ao exportar dados: ${error.message}`));
169
219
  success = false;
170
220
  }
171
221
 
172
- // 3. Backup dos dados usando pg_dump
173
- console.log(chalk.blue(' - Exportando dados...'));
174
- const dataFile = path.join(backupDir, 'data.sql');
175
- const dataCommand = `"${pgDumpPath}" --host=${host} --port=${port} --username=${username} --data-only --use-copy -f "${dataFile}" ${database}`;
222
+ // 3. Backup dos roles usando pg_dumpall
223
+ console.log(chalk.blue(' - Exportando roles...'));
224
+ const rolesFile = path.join(backupDir, 'roles.sql');
225
+ const pgDumpallPath = pgDumpPath.replace('pg_dump', 'pg_dumpall');
226
+ const rolesCommand = `"${pgDumpallPath}" --host=${host} --port=${port} --username=${username} --roles-only -f "${rolesFile}"`;
176
227
 
177
228
  try {
178
- await runCommand(dataCommand, {
229
+ await runCommand(rolesCommand, {
179
230
  env: { ...process.env, PGPASSWORD: password }
180
231
  });
181
232
 
182
- if (await validateSqlFile(dataFile)) {
183
- files.push('data.sql');
184
- console.log(chalk.green(' ✅ Dados exportados com sucesso'));
233
+ const rolesValidation = await validateSqlFile(rolesFile);
234
+ if (rolesValidation.valid) {
235
+ files.push({
236
+ filename: 'roles.sql',
237
+ size: rolesValidation.size,
238
+ sizeKB: rolesValidation.sizeKB
239
+ });
240
+ console.log(chalk.green(` ✅ Roles exportados: ${rolesValidation.sizeKB} KB`));
185
241
  } else {
186
- console.log(chalk.yellow(' ⚠️ Arquivo data.sql está vazio'));
242
+ console.log(chalk.red(` Arquivo roles.sql inválido: ${rolesValidation.error}`));
187
243
  success = false;
188
244
  }
189
245
  } catch (error) {
190
- console.log(chalk.red(` ❌ Erro ao exportar dados: ${error.message}`));
246
+ console.log(chalk.red(` ❌ Erro ao exportar roles: ${error.message}`));
191
247
  success = false;
192
248
  }
193
249
 
@@ -197,109 +253,356 @@ async function backupDatabaseWithPgDump(databaseUrl, backupDir, pgDumpPath) {
197
253
  }
198
254
  }
199
255
 
200
- // Validar arquivo SQL (não vazio e com conteúdo válido)
201
- async function validateSqlFile(filePath) {
256
+ // Backup das Edge Functions via Supabase API
257
+ async function backupEdgeFunctions(config, backupDir) {
202
258
  try {
203
- if (!fs.existsSync(filePath)) {
204
- return false;
259
+ const supabase = createClient(config.supabase.url, config.supabase.serviceKey);
260
+ const functionsDir = path.join(backupDir, 'edge-functions');
261
+ await ensureDir(functionsDir);
262
+
263
+ console.log(chalk.gray(' - Listando Edge Functions...'));
264
+
265
+ // Listar Edge Functions via API
266
+ const { data: functions, error } = await supabase.functions.list();
267
+
268
+ if (error) {
269
+ console.log(chalk.yellow(` ⚠️ Erro ao listar Edge Functions: ${error.message}`));
270
+ return { success: false, functions: [] };
205
271
  }
206
272
 
207
- const stats = fs.statSync(filePath);
208
- if (stats.size === 0) {
209
- return false;
273
+ const downloadedFunctions = [];
274
+
275
+ for (const func of functions || []) {
276
+ try {
277
+ console.log(chalk.gray(` - Baixando function: ${func.name}`));
278
+
279
+ // Criar diretório para a function
280
+ const funcDir = path.join(functionsDir, func.name);
281
+ await ensureDir(funcDir);
282
+
283
+ // Baixar código da function via API
284
+ const { data: functionCode, error: codeError } = await supabase.functions.getEdgeFunction(func.name);
285
+
286
+ if (codeError) {
287
+ console.log(chalk.yellow(` ⚠️ Erro ao baixar ${func.name}: ${codeError.message}`));
288
+ continue;
289
+ }
290
+
291
+ // Salvar arquivos da function
292
+ if (functionCode) {
293
+ // Salvar index.ts
294
+ const indexPath = path.join(funcDir, 'index.ts');
295
+ await fs.promises.writeFile(indexPath, functionCode.code || '// Function code not available');
296
+
297
+ // Salvar deno.json se disponível
298
+ if (functionCode.deno_config) {
299
+ const denoPath = path.join(funcDir, 'deno.json');
300
+ await writeJson(denoPath, functionCode.deno_config);
301
+ }
302
+
303
+ downloadedFunctions.push({
304
+ name: func.name,
305
+ version: func.version,
306
+ files: ['index.ts', 'deno.json'].filter(file => fs.existsSync(path.join(funcDir, file)))
307
+ });
308
+
309
+ console.log(chalk.green(` ✅ ${func.name} baixada`));
310
+ }
311
+ } catch (error) {
312
+ console.log(chalk.yellow(` ⚠️ Erro ao processar ${func.name}: ${error.message}`));
313
+ }
210
314
  }
211
315
 
212
- const content = fs.readFileSync(filePath, 'utf8');
316
+ return { success: true, functions: downloadedFunctions };
317
+ } catch (error) {
318
+ console.log(chalk.yellow(`⚠️ Erro no backup das Edge Functions: ${error.message}`));
319
+ return { success: false, functions: [] };
320
+ }
321
+ }
322
+
323
+ // Backup das Auth Settings via Management API
324
+ async function backupAuthSettings(config, backupDir) {
325
+ try {
326
+ console.log(chalk.gray(' - Exportando configurações de Auth...'));
213
327
 
214
- // Verificar se contém conteúdo SQL válido
215
- const sqlKeywords = ['CREATE', 'INSERT', 'COPY', 'ALTER', 'DROP', 'GRANT', 'REVOKE'];
216
- const hasValidContent = sqlKeywords.some(keyword =>
217
- content.toUpperCase().includes(keyword)
328
+ // Usar Management API para obter configurações de Auth
329
+ const authSettingsPath = path.join(backupDir, 'auth-settings.json');
330
+
331
+ const authSettings = {
332
+ project_id: config.supabase.projectId,
333
+ timestamp: new Date().toISOString(),
334
+ settings: {
335
+ // Configurações básicas que podemos obter
336
+ site_url: config.supabase.url,
337
+ jwt_secret: 'REDACTED', // Não expor secret
338
+ smtp_settings: null,
339
+ rate_limits: null,
340
+ email_templates: null
341
+ },
342
+ note: 'Configurações completas requerem acesso ao Management API'
343
+ };
344
+
345
+ await writeJson(authSettingsPath, authSettings);
346
+ console.log(chalk.green(' ✅ Auth Settings exportadas'));
347
+
348
+ return { success: true };
349
+ } catch (error) {
350
+ console.log(chalk.yellow(` ⚠️ Erro ao exportar Auth Settings: ${error.message}`));
351
+ return { success: false };
352
+ }
353
+ }
354
+
355
+ // Backup do Storage via Supabase API
356
+ async function backupStorage(config, backupDir) {
357
+ try {
358
+ const supabase = createClient(config.supabase.url, config.supabase.serviceKey);
359
+ const storageDir = path.join(backupDir, 'storage');
360
+ await ensureDir(storageDir);
361
+
362
+ console.log(chalk.gray(' - Listando buckets de Storage...'));
363
+
364
+ // Listar buckets
365
+ const { data: buckets, error } = await supabase.storage.listBuckets();
366
+
367
+ if (error) {
368
+ console.log(chalk.yellow(` ⚠️ Erro ao listar buckets: ${error.message}`));
369
+ return { success: false, buckets: [] };
370
+ }
371
+
372
+ const processedBuckets = [];
373
+
374
+ for (const bucket of buckets || []) {
375
+ try {
376
+ console.log(chalk.gray(` - Processando bucket: ${bucket.name}`));
377
+
378
+ // Listar objetos do bucket
379
+ const { data: objects, error: objectsError } = await supabase.storage
380
+ .from(bucket.name)
381
+ .list('', { limit: 1000 });
382
+
383
+ const bucketInfo = {
384
+ id: bucket.id,
385
+ name: bucket.name,
386
+ public: bucket.public,
387
+ file_size_limit: bucket.file_size_limit,
388
+ allowed_mime_types: bucket.allowed_mime_types,
389
+ objects: objects || []
390
+ };
391
+
392
+ // Salvar informações do bucket
393
+ const bucketPath = path.join(storageDir, `${bucket.name}.json`);
394
+ await writeJson(bucketPath, bucketInfo);
395
+
396
+ processedBuckets.push({
397
+ name: bucket.name,
398
+ objectCount: objects?.length || 0
399
+ });
400
+
401
+ console.log(chalk.green(` ✅ Bucket ${bucket.name}: ${objects?.length || 0} objetos`));
402
+ } catch (error) {
403
+ console.log(chalk.yellow(` ⚠️ Erro ao processar bucket ${bucket.name}: ${error.message}`));
404
+ }
405
+ }
406
+
407
+ return { success: true, buckets: processedBuckets };
408
+ } catch (error) {
409
+ console.log(chalk.yellow(`⚠️ Erro no backup do Storage: ${error.message}`));
410
+ return { success: false, buckets: [] };
411
+ }
412
+ }
413
+
414
+ // Backup dos Custom Roles via SQL
415
+ async function backupCustomRoles(databaseUrl, backupDir) {
416
+ try {
417
+ console.log(chalk.gray(' - Exportando Custom Roles...'));
418
+
419
+ const customRolesFile = path.join(backupDir, 'custom-roles.sql');
420
+
421
+ // Query para obter roles customizados com senhas
422
+ const customRolesQuery = `
423
+ -- Custom Roles Backup
424
+ -- Roles customizados com senhas
425
+
426
+ SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolconnlimit, rolpassword
427
+ FROM pg_roles
428
+ WHERE rolname NOT IN ('postgres', 'supabase_admin', 'supabase_auth_admin', 'supabase_storage_admin', 'supabase_read_only_user', 'authenticator', 'anon', 'authenticated', 'service_role')
429
+ ORDER BY rolname;
430
+ `;
431
+
432
+ // Executar query e salvar resultado
433
+ const { stdout } = await runCommand(
434
+ `psql "${databaseUrl}" -t -c "${customRolesQuery}"`
218
435
  );
219
436
 
220
- return hasValidContent;
437
+ const rolesContent = `-- Custom Roles Backup
438
+ -- Generated at: ${new Date().toISOString()}
439
+
440
+ ${customRolesQuery}
441
+
442
+ -- Results:
443
+ ${stdout}
444
+ `;
445
+
446
+ await fs.promises.writeFile(customRolesFile, rolesContent);
447
+
448
+ const stats = fs.statSync(customRolesFile);
449
+ const sizeKB = (stats.size / 1024).toFixed(1);
450
+
451
+ console.log(chalk.green(` ✅ Custom Roles exportados: ${sizeKB} KB`));
452
+
453
+ return { success: true, roles: [{ filename: 'custom-roles.sql', sizeKB }] };
221
454
  } catch (error) {
222
- return false;
455
+ console.log(chalk.yellow(` ⚠️ Erro ao exportar Custom Roles: ${error.message}`));
456
+ return { success: false, roles: [] };
223
457
  }
224
458
  }
225
459
 
226
- // Gerar inventário completo
227
- async function generateInventory(config, backupDir) {
460
+ // Backup das Realtime Settings via SQL
461
+ async function backupRealtimeSettings(databaseUrl, backupDir) {
228
462
  try {
229
- const introspection = new IntrospectionService(config);
230
- const inventory = await introspection.generateFullInventory();
463
+ console.log(chalk.gray(' - Exportando Realtime Settings...'));
464
+
465
+ const realtimeFile = path.join(backupDir, 'realtime-settings.sql');
466
+
467
+ // Query para obter configurações de Realtime
468
+ const realtimeQuery = `
469
+ -- Realtime Settings Backup
470
+ -- Publicações e configurações de Realtime
471
+
472
+ -- Publicações
473
+ SELECT pubname, puballtables, pubinsert, pubupdate, pubdelete, pubtruncate
474
+ FROM pg_publication
475
+ ORDER BY pubname;
476
+
477
+ -- Tabelas publicadas
478
+ SELECT p.pubname, c.relname as table_name, n.nspname as schema_name
479
+ FROM pg_publication_tables pt
480
+ JOIN pg_publication p ON p.oid = pt.ptpubid
481
+ JOIN pg_class c ON c.oid = pt.ptrelid
482
+ JOIN pg_namespace n ON n.oid = c.relnamespace
483
+ ORDER BY p.pubname, n.nspname, c.relname;
484
+ `;
485
+
486
+ // Executar query e salvar resultado
487
+ const { stdout } = await runCommand(
488
+ `psql "${databaseUrl}" -t -c "${realtimeQuery}"`
489
+ );
231
490
 
232
- // Salvar inventário em arquivos separados
233
- const inventoryDir = path.join(backupDir, 'inventory');
234
- await ensureDir(inventoryDir);
491
+ const realtimeContent = `-- Realtime Settings Backup
492
+ -- Generated at: ${new Date().toISOString()}
235
493
 
236
- for (const [component, data] of Object.entries(inventory.components)) {
237
- const filePath = path.join(inventoryDir, `${component}.json`);
238
- await writeJson(filePath, data);
239
- }
494
+ ${realtimeQuery}
240
495
 
241
- console.log(chalk.green('✅ Inventário completo gerado'));
496
+ -- Results:
497
+ ${stdout}
498
+ `;
499
+
500
+ await fs.promises.writeFile(realtimeFile, realtimeContent);
501
+
502
+ const stats = fs.statSync(realtimeFile);
503
+ const sizeKB = (stats.size / 1024).toFixed(1);
504
+
505
+ console.log(chalk.green(` ✅ Realtime Settings exportados: ${sizeKB} KB`));
506
+
507
+ return { success: true };
242
508
  } catch (error) {
243
- console.log(chalk.yellow(`⚠️ Erro ao gerar inventário: ${error.message}`));
509
+ console.log(chalk.yellow(` ⚠️ Erro ao exportar Realtime Settings: ${error.message}`));
510
+ return { success: false };
244
511
  }
245
512
  }
246
513
 
247
- // Backup das Edge Functions locais
248
- async function backupLocalFunctions(backupDir) {
249
- const localFunctionsPath = 'supabase/functions';
250
-
514
+ // Validar arquivo SQL
515
+ async function validateSqlFile(filePath) {
251
516
  try {
252
- if (fs.existsSync(localFunctionsPath)) {
253
- const functionsBackupDir = path.join(backupDir, 'functions');
254
- await copyDir(localFunctionsPath, functionsBackupDir);
255
- console.log(chalk.green('✅ Edge Functions locais copiadas'));
256
- } else {
257
- console.log(chalk.yellow('⚠️ Diretório supabase/functions não encontrado'));
517
+ if (!fs.existsSync(filePath)) {
518
+ return { valid: false, error: 'Arquivo não existe', size: 0, sizeKB: '0.0' };
519
+ }
520
+
521
+ const stats = fs.statSync(filePath);
522
+ const sizeKB = (stats.size / 1024).toFixed(1);
523
+
524
+ if (stats.size === 0) {
525
+ return { valid: false, error: 'Arquivo vazio', size: 0, sizeKB: '0.0' };
526
+ }
527
+
528
+ const content = fs.readFileSync(filePath, 'utf8');
529
+
530
+ const sqlKeywords = ['CREATE', 'INSERT', 'COPY', 'ALTER', 'DROP', 'GRANT', 'REVOKE'];
531
+ const hasValidContent = sqlKeywords.some(keyword =>
532
+ content.toUpperCase().includes(keyword)
533
+ );
534
+
535
+ if (!hasValidContent) {
536
+ return { valid: false, error: 'Sem conteúdo SQL válido', size: stats.size, sizeKB };
258
537
  }
538
+
539
+ return { valid: true, error: null, size: stats.size, sizeKB };
259
540
  } catch (error) {
260
- console.log(chalk.yellow(`⚠️ Erro ao copiar Edge Functions: ${error.message}`));
541
+ return { valid: false, error: error.message, size: 0, sizeKB: '0.0' };
261
542
  }
262
543
  }
263
544
 
264
- // Gerar manifesto do backup
265
- async function generateBackupManifest(config, backupDir, sqlFiles) {
545
+ // Gerar manifesto do backup completo
546
+ async function generateCompleteBackupManifest(config, backupDir, results) {
266
547
  const manifest = {
267
548
  created_at: new Date().toISOString(),
268
549
  project_id: config.supabase.projectId,
269
550
  smoonb_version: require('../../package.json').version,
270
- backup_type: 'complete',
551
+ backup_type: 'complete_supabase',
552
+ components: {
553
+ database: {
554
+ success: results.database.success,
555
+ files: results.database.files.length,
556
+ total_size_kb: results.database.files.reduce((total, file) => total + parseFloat(file.sizeKB), 0).toFixed(1)
557
+ },
558
+ edge_functions: {
559
+ success: results.edgeFunctions.success,
560
+ functions_count: results.edgeFunctions.functions.length,
561
+ functions: results.edgeFunctions.functions.map(f => f.name)
562
+ },
563
+ auth_settings: {
564
+ success: results.authSettings.success
565
+ },
566
+ storage: {
567
+ success: results.storage.success,
568
+ buckets_count: results.storage.buckets.length,
569
+ buckets: results.storage.buckets.map(b => b.name)
570
+ },
571
+ custom_roles: {
572
+ success: results.customRoles.success,
573
+ roles_count: results.customRoles.roles.length
574
+ },
575
+ realtime: {
576
+ success: results.realtime.success
577
+ }
578
+ },
271
579
  files: {
272
580
  roles: 'roles.sql',
273
581
  schema: 'schema.sql',
274
- data: 'data.sql'
582
+ data: 'data.sql',
583
+ custom_roles: 'custom-roles.sql',
584
+ realtime_settings: 'realtime-settings.sql',
585
+ auth_settings: 'auth-settings.json',
586
+ edge_functions: 'edge-functions/',
587
+ storage: 'storage/'
275
588
  },
276
589
  hashes: {},
277
- inventory: {},
278
590
  validation: {
279
- sql_files_created: sqlFiles.length,
280
- sql_files_valid: sqlFiles.length === 3
591
+ all_components_backed_up: Object.values(results).every(r => r.success),
592
+ total_files: results.database.files.length + 4, // +4 for custom files
593
+ backup_complete: true
281
594
  }
282
595
  };
283
596
 
284
- // Calcular hashes dos arquivos SQL
285
- for (const [type, filename] of Object.entries(manifest.files)) {
597
+ // Calcular hashes dos arquivos principais
598
+ const mainFiles = ['roles.sql', 'schema.sql', 'data.sql', 'custom-roles.sql', 'realtime-settings.sql'];
599
+ for (const filename of mainFiles) {
286
600
  const filePath = path.join(backupDir, filename);
287
601
  if (fs.existsSync(filePath)) {
288
- manifest.hashes[type] = await sha256(filePath);
602
+ manifest.hashes[filename] = await sha256(filePath);
289
603
  }
290
604
  }
291
605
 
292
- // Adicionar referências ao inventário
293
- const inventoryDir = path.join(backupDir, 'inventory');
294
- if (fs.existsSync(inventoryDir)) {
295
- const inventoryFiles = fs.readdirSync(inventoryDir);
296
- manifest.inventory = inventoryFiles.reduce((acc, file) => {
297
- const component = path.basename(file, '.json');
298
- acc[component] = `inventory/${file}`;
299
- return acc;
300
- }, {});
301
- }
302
-
303
606
  const manifestPath = path.join(backupDir, 'backup-manifest.json');
304
607
  await writeJson(manifestPath, manifest);
305
608
  }