smoonb 0.0.26 → 0.0.28

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.26",
3
+ "version": "0.0.28",
4
4
  "description": "Complete Supabase backup and migration tool - EXPERIMENTAL VERSION - USE AT YOUR OWN RISK",
5
5
  "preferGlobal": false,
6
6
  "preventGlobalInstall": true,
@@ -108,38 +108,37 @@ async function performFullBackup(config, options) {
108
108
  };
109
109
 
110
110
  // 1. Backup Database via pg_dumpall Docker (idêntico ao Dashboard)
111
- console.log(chalk.blue('\n📊 1/6 - Backup da Database PostgreSQL via pg_dumpall Docker...'));
112
- const dbResult = await backupDatabaseWithDocker(config.supabase.databaseUrl, backupDir);
113
- manifest.components.database = {
114
- success: dbResult.success,
115
- method: 'pg_dumpall_docker',
116
- fileName: dbResult.fileName,
117
- size_kb: dbResult.size,
118
- dashboard_compatible: true
119
- };
111
+ console.log(chalk.blue('\n📊 1/7 - Backup da Database PostgreSQL via pg_dumpall Docker...'));
112
+ const databaseResult = await backupDatabase(config.supabase.projectId, backupDir);
113
+ manifest.components.database = databaseResult;
120
114
 
121
115
  // 2. Backup Edge Functions via Docker
122
- console.log(chalk.blue('\n⚡ 2/6 - Backup das Edge Functions via Docker...'));
116
+ console.log(chalk.blue('\n⚡ 2/7 - Backup das Edge Functions via Docker...'));
123
117
  const functionsResult = await backupEdgeFunctionsWithDocker(config.supabase.projectId, config.supabase.accessToken, backupDir);
124
118
  manifest.components.edge_functions = functionsResult;
125
119
 
126
120
  // 3. Backup Auth Settings via API
127
- console.log(chalk.blue('\n🔐 3/6 - Backup das Auth Settings via API...'));
121
+ console.log(chalk.blue('\n🔐 3/7 - Backup das Auth Settings via API...'));
128
122
  const authResult = await backupAuthSettings(config.supabase.projectId, config.supabase.accessToken, backupDir);
129
123
  manifest.components.auth_settings = authResult;
130
124
 
131
125
  // 4. Backup Storage via API
132
- console.log(chalk.blue('\n📦 4/6 - Backup do Storage via API...'));
126
+ console.log(chalk.blue('\n📦 4/7 - Backup do Storage via API...'));
133
127
  const storageResult = await backupStorage(config.supabase.projectId, config.supabase.accessToken, backupDir);
134
128
  manifest.components.storage = storageResult;
135
129
 
136
130
  // 5. Backup Custom Roles via SQL
137
- console.log(chalk.blue('\n👥 5/6 - Backup dos Custom Roles via SQL...'));
131
+ console.log(chalk.blue('\n👥 5/7 - Backup dos Custom Roles via SQL...'));
138
132
  const rolesResult = await backupCustomRoles(config.supabase.databaseUrl, backupDir);
139
133
  manifest.components.custom_roles = rolesResult;
140
134
 
141
- // 6. Backup Realtime Settings via Captura Interativa
142
- console.log(chalk.blue('\n🔄 6/6 - Backup das Realtime Settings via Captura Interativa...'));
135
+ // 6. Backup das Database Extensions and Settings via SQL
136
+ console.log(chalk.blue('\n🔧 6/7 - Backup das Database Extensions and Settings via SQL...'));
137
+ const databaseSettingsResult = await backupDatabaseSettings(config.supabase.projectId, backupDir);
138
+ manifest.components.database_settings = databaseSettingsResult;
139
+
140
+ // 7. Backup Realtime Settings via Captura Interativa
141
+ console.log(chalk.blue('\n🔄 7/7 - Backup das Realtime Settings via Captura Interativa...'));
143
142
  const realtimeResult = await backupRealtimeSettings(config.supabase.projectId, backupDir, options.skipRealtime);
144
143
  manifest.components.realtime = realtimeResult;
145
144
 
@@ -148,12 +147,22 @@ async function performFullBackup(config, options) {
148
147
 
149
148
  console.log(chalk.green('\n🎉 BACKUP COMPLETO FINALIZADO VIA DOCKER!'));
150
149
  console.log(chalk.blue(`📁 Localização: ${backupDir}`));
151
- console.log(chalk.green(`📊 Database: ${dbResult.fileName} (${dbResult.size} KB) - Idêntico ao Dashboard`));
150
+ console.log(chalk.green(`📊 Database: ${databaseResult.fileName} (${databaseResult.size} KB) - Idêntico ao Dashboard`));
151
+ console.log(chalk.green(`🔧 Database Settings: ${databaseSettingsResult.fileName} (${databaseSettingsResult.size} KB) - Extensions e Configurações`));
152
152
  console.log(chalk.green(`⚡ Edge Functions: ${functionsResult.success_count || 0}/${functionsResult.functions_count || 0} functions baixadas via Docker`));
153
153
  console.log(chalk.green(`🔐 Auth Settings: ${authResult.success ? 'Exportadas via API' : 'Falharam'}`));
154
154
  console.log(chalk.green(`📦 Storage: ${storageResult.buckets?.length || 0} buckets verificados via API`));
155
155
  console.log(chalk.green(`👥 Custom Roles: ${rolesResult.roles?.length || 0} roles exportados via SQL`));
156
- console.log(chalk.green(`🔄 Realtime: ${realtimeResult.success ? 'Configurações capturadas interativamente' : 'Falharam'}`));
156
+ // Determinar mensagem correta baseada no método usado
157
+ let realtimeMessage = 'Falharam';
158
+ if (realtimeResult.success) {
159
+ if (options.skipRealtime) {
160
+ realtimeMessage = 'Configurações copiadas do backup anterior';
161
+ } else {
162
+ realtimeMessage = 'Configurações capturadas interativamente';
163
+ }
164
+ }
165
+ console.log(chalk.green(`🔄 Realtime: ${realtimeMessage}`));
157
166
 
158
167
  return { success: true, backupDir, manifest };
159
168
  }
@@ -220,12 +229,16 @@ function showDockerMessagesAndExit(reason) {
220
229
  }
221
230
 
222
231
  // Backup da database usando pg_dumpall via Docker (idêntico ao Supabase Dashboard)
223
- async function backupDatabaseWithDocker(databaseUrl, backupDir) {
232
+ async function backupDatabase(projectId, backupDir) {
224
233
  try {
225
- console.log(chalk.gray('🐳 Criando backup completo via pg_dumpall Docker...'));
234
+ console.log(chalk.gray(' - Criando backup completo via pg_dumpall...'));
235
+
236
+ const { execSync } = require('child_process');
237
+ const config = await readConfig();
226
238
 
227
239
  // Extrair credenciais da databaseUrl
228
- const urlMatch = databaseUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
240
+ const dbUrl = config.supabase.databaseUrl;
241
+ const urlMatch = dbUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
229
242
 
230
243
  if (!urlMatch) {
231
244
  throw new Error('Database URL inválida');
@@ -233,7 +246,7 @@ async function backupDatabaseWithDocker(databaseUrl, backupDir) {
233
246
 
234
247
  const [, username, password, host, port, database] = urlMatch;
235
248
 
236
- // Gerar nome do arquivo igual ao dashboard Supabase
249
+ // Gerar nome do arquivo igual ao dashboard
237
250
  const now = new Date();
238
251
  const day = String(now.getDate()).padStart(2, '0');
239
252
  const month = String(now.getMonth() + 1).padStart(2, '0');
@@ -243,14 +256,14 @@ async function backupDatabaseWithDocker(databaseUrl, backupDir) {
243
256
  const seconds = String(now.getSeconds()).padStart(2, '0');
244
257
 
245
258
  const fileName = `db_cluster-${day}-${month}-${year}@${hours}-${minutes}-${seconds}.backup`;
246
- const filePath = path.join(backupDir, fileName);
247
259
 
248
- console.log(chalk.gray(` - Arquivo: ${fileName}`));
260
+ // CORREÇÃO: Usar caminho absoluto igual às Edge Functions
261
+ const backupDirAbs = path.resolve(backupDir);
249
262
 
250
- // Comando pg_dumpall via Docker (idêntico ao dashboard)
263
+ // Comando pg_dumpall via Docker (mesma abordagem das Edge Functions)
251
264
  const dockerCmd = [
252
265
  'docker run --rm --network host',
253
- `-v "${backupDir}:/host"`,
266
+ `-v "${backupDirAbs}:/host"`,
254
267
  `-e PGPASSWORD="${password}"`,
255
268
  'postgres:17 pg_dumpall',
256
269
  `-h ${host}`,
@@ -259,43 +272,28 @@ async function backupDatabaseWithDocker(databaseUrl, backupDir) {
259
272
  `-f /host/${fileName}`
260
273
  ].join(' ');
261
274
 
262
- console.log(chalk.gray(' - Executando pg_dumpall via Docker...'));
263
- await execAsync(dockerCmd, { stdio: 'pipe' });
275
+ console.log(chalk.gray(` - Executando pg_dumpall via Docker...`));
276
+ execSync(dockerCmd, { stdio: 'pipe' });
264
277
 
265
278
  // Compactar igual ao Supabase Dashboard
266
- console.log(chalk.gray(' - Compactando arquivo...'));
267
279
  const gzipCmd = [
268
280
  'docker run --rm',
269
- `-v "${backupDir}:/host"`,
270
- 'postgres:17 gzip /host/' + fileName
281
+ `-v "${backupDirAbs}:/host"`,
282
+ `postgres:17 gzip /host/${fileName}`
271
283
  ].join(' ');
272
284
 
273
- await execAsync(gzipCmd, { stdio: 'pipe' });
285
+ execSync(gzipCmd, { stdio: 'pipe' });
274
286
 
275
287
  const finalFileName = `${fileName}.gz`;
276
- const finalFilePath = path.join(backupDir, finalFileName);
277
-
278
- // Validar arquivo gerado
279
- if (!fs.existsSync(finalFilePath)) {
280
- throw new Error('Arquivo de backup não foi criado');
281
- }
282
-
283
- const stats = fs.statSync(finalFilePath);
288
+ const stats = fs.statSync(path.join(backupDir, finalFileName));
284
289
  const sizeKB = (stats.size / 1024).toFixed(1);
285
290
 
286
291
  console.log(chalk.green(` ✅ Database backup: ${finalFileName} (${sizeKB} KB)`));
287
292
 
288
- return {
289
- success: true,
290
- fileName: finalFileName,
291
- size: sizeKB,
292
- method: 'pg_dumpall_docker',
293
- dashboard_compatible: true
294
- };
295
-
293
+ return { success: true, size: sizeKB, fileName: finalFileName };
296
294
  } catch (error) {
297
- console.log(chalk.red(` Erro no backup do database: ${error.message}`));
298
- return { success: false, error: error.message };
295
+ console.log(chalk.yellow(` ⚠️ Erro no backup do database: ${error.message}`));
296
+ return { success: false };
299
297
  }
300
298
  }
301
299
 
@@ -541,6 +539,187 @@ async function backupCustomRoles(databaseUrl, backupDir) {
541
539
  }
542
540
  }
543
541
 
542
+ // Backup das Database Extensions and Settings via SQL
543
+ async function backupDatabaseSettings(projectId, backupDir) {
544
+ try {
545
+ console.log(chalk.gray(' - Capturando Database Extensions and Settings...'));
546
+
547
+ const { execSync } = require('child_process');
548
+ const config = await readConfig();
549
+
550
+ // Extrair credenciais da databaseUrl
551
+ const dbUrl = config.supabase.databaseUrl;
552
+ const urlMatch = dbUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
553
+
554
+ if (!urlMatch) {
555
+ throw new Error('Database URL inválida');
556
+ }
557
+
558
+ const [, username, password, host, port, database] = urlMatch;
559
+
560
+ // Gerar nome do arquivo
561
+ const now = new Date();
562
+ const day = String(now.getDate()).padStart(2, '0');
563
+ const month = String(now.getMonth() + 1).padStart(2, '0');
564
+ const year = now.getFullYear();
565
+ const hours = String(now.getHours()).padStart(2, '0');
566
+ const minutes = String(now.getMinutes()).padStart(2, '0');
567
+ const seconds = String(now.getSeconds()).padStart(2, '0');
568
+
569
+ const fileName = `database-settings-${day}-${month}-${year}@${hours}-${minutes}-${seconds}.json`;
570
+
571
+ // Usar caminho absoluto igual às outras funções
572
+ const backupDirAbs = path.resolve(backupDir);
573
+
574
+ // Script SQL para capturar todas as configurações
575
+ const sqlScript = `
576
+ -- Database Extensions and Settings Backup
577
+ -- Generated at: ${new Date().toISOString()}
578
+
579
+ -- 1. Capturar extensões instaladas
580
+ SELECT json_agg(
581
+ json_build_object(
582
+ 'name', extname,
583
+ 'version', extversion,
584
+ 'schema', extnamespace::regnamespace
585
+ )
586
+ ) as extensions
587
+ FROM pg_extension
588
+ ORDER BY extname;
589
+
590
+ -- 2. Capturar configurações PostgreSQL importantes
591
+ SELECT json_agg(
592
+ json_build_object(
593
+ 'name', name,
594
+ 'setting', setting,
595
+ 'unit', unit,
596
+ 'context', context,
597
+ 'description', short_desc
598
+ )
599
+ ) as postgres_settings
600
+ FROM pg_settings
601
+ WHERE name IN (
602
+ 'statement_timeout',
603
+ 'idle_in_transaction_session_timeout',
604
+ 'lock_timeout',
605
+ 'shared_buffers',
606
+ 'work_mem',
607
+ 'maintenance_work_mem',
608
+ 'effective_cache_size',
609
+ 'max_connections',
610
+ 'log_statement',
611
+ 'log_min_duration_statement',
612
+ 'timezone',
613
+ 'log_timezone',
614
+ 'default_transaction_isolation',
615
+ 'default_transaction_read_only',
616
+ 'checkpoint_completion_target',
617
+ 'wal_buffers',
618
+ 'max_wal_size',
619
+ 'min_wal_size'
620
+ )
621
+ ORDER BY name;
622
+
623
+ -- 3. Capturar configurações específicas dos roles Supabase
624
+ SELECT json_agg(
625
+ json_build_object(
626
+ 'role', rolname,
627
+ 'config', rolconfig
628
+ )
629
+ ) as role_configurations
630
+ FROM pg_roles
631
+ WHERE rolname IN ('anon', 'authenticated', 'authenticator', 'postgres', 'service_role')
632
+ AND rolconfig IS NOT NULL
633
+ ORDER BY rolname;
634
+
635
+ -- 4. Capturar configurações de PGAudit (se existir)
636
+ SELECT json_agg(
637
+ json_build_object(
638
+ 'role', rolname,
639
+ 'config', rolconfig
640
+ )
641
+ ) as pgaudit_configurations
642
+ FROM pg_roles
643
+ WHERE rolconfig IS NOT NULL
644
+ AND EXISTS (
645
+ SELECT 1 FROM unnest(rolconfig) AS config
646
+ WHERE config LIKE '%pgaudit%'
647
+ )
648
+ ORDER BY rolname;
649
+ `;
650
+
651
+ // Salvar script SQL temporário
652
+ const sqlFile = path.join(backupDir, 'temp_settings.sql');
653
+ await fs.writeFile(sqlFile, sqlScript);
654
+
655
+ // Executar via Docker
656
+ const dockerCmd = [
657
+ 'docker run --rm --network host',
658
+ `-v "${backupDirAbs}:/host"`,
659
+ `-e PGPASSWORD="${password}"`,
660
+ 'postgres:17 psql',
661
+ `-h ${host}`,
662
+ `-p ${port}`,
663
+ `-U ${username}`,
664
+ `-d ${database}`,
665
+ '-f /host/temp_settings.sql',
666
+ '-t', // Tuples only
667
+ '-A', // Unaligned output
668
+ '-F', // Field separator
669
+ '|'
670
+ ].join(' ');
671
+
672
+ console.log(chalk.gray(' - Executando queries de configurações via Docker...'));
673
+ const output = execSync(dockerCmd, { stdio: 'pipe', encoding: 'utf8' });
674
+
675
+ // Processar output e criar JSON estruturado
676
+ const lines = output.trim().split('\n').filter(line => line.trim());
677
+
678
+ const result = {
679
+ database_settings: {
680
+ note: "Configurações específicas do database Supabase capturadas via SQL",
681
+ captured_at: new Date().toISOString(),
682
+ project_id: projectId,
683
+ extensions: lines[0] ? JSON.parse(lines[0]) : [],
684
+ postgres_settings: lines[1] ? JSON.parse(lines[1]) : [],
685
+ role_configurations: lines[2] ? JSON.parse(lines[2]) : [],
686
+ pgaudit_configurations: lines[3] ? JSON.parse(lines[3]) : [],
687
+ restore_instructions: {
688
+ note: "Estas configurações precisam ser aplicadas manualmente após a restauração do database",
689
+ steps: [
690
+ "1. Restaurar o database usando o arquivo .backup.gz",
691
+ "2. Aplicar configurações de Postgres via SQL:",
692
+ " ALTER DATABASE postgres SET setting_name TO 'value';",
693
+ "3. Aplicar configurações de roles via SQL:",
694
+ " ALTER ROLE role_name SET setting_name TO 'value';",
695
+ "4. Habilitar extensões necessárias via Dashboard ou SQL:",
696
+ " CREATE EXTENSION IF NOT EXISTS extension_name;",
697
+ "5. Verificar configurações aplicadas:",
698
+ " SELECT name, setting FROM pg_settings WHERE name IN (...);"
699
+ ]
700
+ }
701
+ }
702
+ };
703
+
704
+ // Salvar arquivo JSON
705
+ const jsonFile = path.join(backupDir, fileName);
706
+ await fs.writeFile(jsonFile, JSON.stringify(result, null, 2));
707
+
708
+ // Limpar arquivo temporário
709
+ await fs.unlink(sqlFile);
710
+
711
+ const stats = fs.statSync(jsonFile);
712
+ const sizeKB = (stats.size / 1024).toFixed(1);
713
+
714
+ console.log(chalk.green(` ✅ Database Settings: ${fileName} (${sizeKB} KB)`));
715
+
716
+ return { success: true, size: sizeKB, fileName: fileName };
717
+ } catch (error) {
718
+ console.log(chalk.yellow(` ⚠️ Erro no backup das Database Settings: ${error.message}`));
719
+ return { success: false };
720
+ }
721
+ }
722
+
544
723
  // Backup das Realtime Settings via Captura Interativa
545
724
  async function backupRealtimeSettings(projectId, backupDir, skipInteractive = false) {
546
725
  try {
@@ -17,7 +17,7 @@ async function captureRealtimeSettings(projectId, backupDir, skipInteractive = f
17
17
  const previousSettings = await getPreviousRealtimeSettings(backupDir);
18
18
 
19
19
  if (skipInteractive && previousSettings) {
20
- console.log('📋 Usando configurações de Realtime Settings do backup anterior...');
20
+ console.log('📋 Copiando Realtime Settings do backup anterior...');
21
21
  await fs.writeFile(settingsFile, JSON.stringify(previousSettings, null, 2));
22
22
  return previousSettings;
23
23
  }