smoonb 0.0.13 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoonb",
3
- "version": "0.0.13",
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": {
@@ -6,6 +6,7 @@ const { ensureDir, writeJson, copyDir } = require('../utils/fsx');
6
6
  const { sha256 } = require('../utils/hash');
7
7
  const { readConfig, validateFor } = require('../utils/config');
8
8
  const { showBetaBanner } = require('../utils/banner');
9
+ const { createClient } = require('@supabase/supabase-js');
9
10
 
10
11
  // Exportar FUNÇÃO em vez de objeto Command
11
12
  module.exports = async (options) => {
@@ -35,17 +36,24 @@ module.exports = async (options) => {
35
36
  // Resolver diretório de saída
36
37
  const outputDir = options.output || config.backup.outputDir;
37
38
 
38
- // Criar diretório de backup com timestamp
39
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
40
- 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}`);
41
49
  await ensureDir(backupDir);
42
50
 
43
- console.log(chalk.blue(`🚀 Iniciando backup do projeto: ${config.supabase.projectId}`));
51
+ console.log(chalk.blue(`🚀 Iniciando backup COMPLETO do projeto: ${config.supabase.projectId}`));
44
52
  console.log(chalk.blue(`📁 Diretório: ${backupDir}`));
45
53
  console.log(chalk.gray(`🔧 Usando pg_dump: ${pgDumpPath}`));
46
54
 
47
- // 1. Backup da Database usando APENAS pg_dump/pg_dumpall
48
- console.log(chalk.blue('\n📊 1/2 - Backup da Database PostgreSQL...'));
55
+ // 1. Backup da Database PostgreSQL (básico)
56
+ console.log(chalk.blue('\n📊 1/6 - Backup da Database PostgreSQL...'));
49
57
  const dbBackupResult = await backupDatabaseWithPgDump(databaseUrl, backupDir, pgDumpPath);
50
58
 
51
59
  if (!dbBackupResult.success) {
@@ -57,24 +65,52 @@ module.exports = async (options) => {
57
65
  process.exit(1);
58
66
  }
59
67
 
60
- // 2. Backup das Edge Functions locais (se existirem)
61
- console.log(chalk.blue('\n⚡ 2/2 - Backup das Edge Functions locais...'));
62
- await backupLocalFunctions(backupDir);
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);
63
83
 
64
- // Gerar manifesto do backup
65
- await generateBackupManifest(config, backupDir, dbBackupResult.files);
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);
66
87
 
67
- console.log(chalk.green('\n🎉 Backup completo finalizado!'));
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!'));
68
99
  console.log(chalk.blue(`📁 Localização: ${backupDir}`));
69
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'}`));
70
106
 
71
107
  // Mostrar resumo dos arquivos
72
108
  console.log(chalk.blue('\n📊 Resumo dos arquivos gerados:'));
73
109
  for (const file of dbBackupResult.files) {
74
- const filePath = path.join(backupDir, file.filename);
75
- const stats = fs.statSync(filePath);
76
- const sizeKB = (stats.size / 1024).toFixed(1);
77
- console.log(chalk.gray(` - ${file.filename}: ${sizeKB} KB`));
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`));
78
114
  }
79
115
 
80
116
  } catch (error) {
@@ -111,7 +147,7 @@ async function findPgDumpPath() {
111
147
  return null;
112
148
  }
113
149
 
114
- // Backup da database usando APENAS pg_dump/pg_dumpall
150
+ // Backup da database usando pg_dump/pg_dumpall
115
151
  async function backupDatabaseWithPgDump(databaseUrl, backupDir, pgDumpPath) {
116
152
  try {
117
153
  // Parse da URL da database
@@ -129,7 +165,7 @@ async function backupDatabaseWithPgDump(databaseUrl, backupDir, pgDumpPath) {
129
165
  const files = [];
130
166
  let success = true;
131
167
 
132
- // 1. Backup do schema usando pg_dump (COMANDO VALIDADO)
168
+ // 1. Backup do schema usando pg_dump
133
169
  console.log(chalk.blue(' - Exportando schema...'));
134
170
  const schemaFile = path.join(backupDir, 'schema.sql');
135
171
  const schemaCommand = `"${pgDumpPath}" "${databaseUrl}" --schema-only -f "${schemaFile}"`;
@@ -156,7 +192,7 @@ async function backupDatabaseWithPgDump(databaseUrl, backupDir, pgDumpPath) {
156
192
  success = false;
157
193
  }
158
194
 
159
- // 2. Backup dos dados usando pg_dump (COMANDO VALIDADO)
195
+ // 2. Backup dos dados usando pg_dump
160
196
  console.log(chalk.blue(' - Exportando dados...'));
161
197
  const dataFile = path.join(backupDir, 'data.sql');
162
198
  const dataCommand = `"${pgDumpPath}" "${databaseUrl}" --data-only -f "${dataFile}"`;
@@ -183,7 +219,7 @@ async function backupDatabaseWithPgDump(databaseUrl, backupDir, pgDumpPath) {
183
219
  success = false;
184
220
  }
185
221
 
186
- // 3. Backup dos roles usando pg_dumpall (COMANDO VALIDADO)
222
+ // 3. Backup dos roles usando pg_dumpall
187
223
  console.log(chalk.blue(' - Exportando roles...'));
188
224
  const rolesFile = path.join(backupDir, 'roles.sql');
189
225
  const pgDumpallPath = pgDumpPath.replace('pg_dump', 'pg_dumpall');
@@ -217,7 +253,265 @@ async function backupDatabaseWithPgDump(databaseUrl, backupDir, pgDumpPath) {
217
253
  }
218
254
  }
219
255
 
220
- // Validar arquivo SQL (não vazio e com conteúdo válido)
256
+ // Backup das Edge Functions via Supabase API
257
+ async function backupEdgeFunctions(config, backupDir) {
258
+ try {
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: [] };
271
+ }
272
+
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
+ }
314
+ }
315
+
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...'));
327
+
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}"`
435
+ );
436
+
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 }] };
454
+ } catch (error) {
455
+ console.log(chalk.yellow(` ⚠️ Erro ao exportar Custom Roles: ${error.message}`));
456
+ return { success: false, roles: [] };
457
+ }
458
+ }
459
+
460
+ // Backup das Realtime Settings via SQL
461
+ async function backupRealtimeSettings(databaseUrl, backupDir) {
462
+ try {
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
+ );
490
+
491
+ const realtimeContent = `-- Realtime Settings Backup
492
+ -- Generated at: ${new Date().toISOString()}
493
+
494
+ ${realtimeQuery}
495
+
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 };
508
+ } catch (error) {
509
+ console.log(chalk.yellow(` ⚠️ Erro ao exportar Realtime Settings: ${error.message}`));
510
+ return { success: false };
511
+ }
512
+ }
513
+
514
+ // Validar arquivo SQL
221
515
  async function validateSqlFile(filePath) {
222
516
  try {
223
517
  if (!fs.existsSync(filePath)) {
@@ -233,7 +527,6 @@ async function validateSqlFile(filePath) {
233
527
 
234
528
  const content = fs.readFileSync(filePath, 'utf8');
235
529
 
236
- // Verificar se contém conteúdo SQL válido
237
530
  const sqlKeywords = ['CREATE', 'INSERT', 'COPY', 'ALTER', 'DROP', 'GRANT', 'REVOKE'];
238
531
  const hasValidContent = sqlKeywords.some(keyword =>
239
532
  content.toUpperCase().includes(keyword)
@@ -249,48 +542,64 @@ async function validateSqlFile(filePath) {
249
542
  }
250
543
  }
251
544
 
252
- // Backup das Edge Functions locais (se existirem)
253
- async function backupLocalFunctions(backupDir) {
254
- const localFunctionsPath = 'supabase/functions';
255
-
256
- try {
257
- if (fs.existsSync(localFunctionsPath)) {
258
- const functionsBackupDir = path.join(backupDir, 'functions');
259
- await copyDir(localFunctionsPath, functionsBackupDir);
260
- console.log(chalk.green('✅ Edge Functions locais copiadas'));
261
- } else {
262
- console.log(chalk.yellow('⚠️ Diretório supabase/functions não encontrado'));
263
- }
264
- } catch (error) {
265
- console.log(chalk.yellow(`⚠️ Erro ao copiar Edge Functions: ${error.message}`));
266
- }
267
- }
268
-
269
- // Gerar manifesto do backup
270
- async function generateBackupManifest(config, backupDir, sqlFiles) {
545
+ // Gerar manifesto do backup completo
546
+ async function generateCompleteBackupManifest(config, backupDir, results) {
271
547
  const manifest = {
272
548
  created_at: new Date().toISOString(),
273
549
  project_id: config.supabase.projectId,
274
550
  smoonb_version: require('../../package.json').version,
275
- backup_type: 'postgresql_native',
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
+ },
276
579
  files: {
277
580
  roles: 'roles.sql',
278
581
  schema: 'schema.sql',
279
- 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/'
280
588
  },
281
589
  hashes: {},
282
590
  validation: {
283
- sql_files_created: sqlFiles.length,
284
- sql_files_valid: sqlFiles.length === 3,
285
- total_size_kb: sqlFiles.reduce((total, file) => total + parseFloat(file.sizeKB), 0).toFixed(1)
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
286
594
  }
287
595
  };
288
596
 
289
- // Calcular hashes dos arquivos SQL
290
- 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) {
291
600
  const filePath = path.join(backupDir, filename);
292
601
  if (fs.existsSync(filePath)) {
293
- manifest.hashes[type] = await sha256(filePath);
602
+ manifest.hashes[filename] = await sha256(filePath);
294
603
  }
295
604
  }
296
605