UNPKG

smoonb

Version:

Complete Supabase backup and migration tool - EXPERIMENTAL VERSION - USE AT YOUR OWN RISK

848 lines (709 loc) 34.4 kB
const chalk = require('chalk'); const path = require('path'); const fs = require('fs').promises; const { exec } = require('child_process'); const { promisify } = require('util'); const { ensureDir, writeJson, copyDir } = require('../utils/fsx'); const { sha256 } = require('../utils/hash'); const { readConfig, validateFor } = require('../utils/config'); const { showBetaBanner } = require('../utils/banner'); const { canPerformCompleteBackup, getDockerVersion } = require('../utils/docker'); const { captureRealtimeSettings } = require('../utils/realtime-settings'); const execAsync = promisify(exec); // Exportar FUNÇÃO em vez de objeto Command module.exports = async (options) => { showBetaBanner(); try { // Carregar e validar configuração const config = await readConfig(); validateFor(config, 'backup'); // Validação adicional para pré-requisitos obrigatórios if (!config.supabase.databaseUrl) { console.log(chalk.red('❌ DATABASE_URL NÃO CONFIGURADA')); console.log(''); console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:')); console.log(chalk.yellow(' 1. Configurar databaseUrl no .smoonbrc')); console.log(chalk.yellow(' 2. Repetir o comando de backup')); console.log(''); console.log(chalk.blue('💡 Exemplo de configuração:')); console.log(chalk.gray(' "databaseUrl": "postgresql://postgres:[senha]@db.[projeto].supabase.co:5432/postgres"')); console.log(''); console.log(chalk.red('🚫 Backup cancelado - Configuração incompleta')); process.exit(1); } if (!config.supabase.accessToken) { console.log(chalk.red('❌ ACCESS_TOKEN NÃO CONFIGURADO')); console.log(''); console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:')); console.log(chalk.yellow(' 1. Obter Personal Access Token do Supabase')); console.log(chalk.yellow(' 2. Configurar accessToken no .smoonbrc')); console.log(chalk.yellow(' 3. Repetir o comando de backup')); console.log(''); console.log(chalk.blue('🔗 Como obter o token:')); console.log(chalk.gray(' 1. Acesse: https://supabase.com/dashboard/account/tokens')); console.log(chalk.gray(' 2. Clique: "Generate new token"')); console.log(chalk.gray(' 3. Copie o token (formato: sbp_...)')); console.log(''); console.log(chalk.red('🚫 Backup cancelado - Token não configurado')); process.exit(1); } console.log(chalk.blue(`🚀 Iniciando backup do projeto: ${config.supabase.projectId}`)); console.log(chalk.gray(`🔍 Verificando dependências Docker...`)); // Verificar se é possível fazer backup completo via Docker const backupCapability = await canPerformCompleteBackup(); if (backupCapability.canBackupComplete) { console.log(chalk.green('✅ Docker Desktop detectado e funcionando')); console.log(chalk.gray(`🐳 Versão: ${backupCapability.dockerStatus.version}`)); console.log(''); // Proceder com backup completo via Docker return await performFullBackup(config, options); } else { // Mostrar mensagens educativas e encerrar elegantemente showDockerMessagesAndExit(backupCapability.reason); } } catch (error) { console.error(chalk.red(`❌ Erro no backup: ${error.message}`)); process.exit(1); } }; // Função para backup completo via Docker async function performFullBackup(config, options) { // Resolver diretório de saída const outputDir = options.output || config.backup.outputDir; // Criar diretório de backup com timestamp humanizado const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hour = String(now.getHours()).padStart(2, '0'); const minute = String(now.getMinutes()).padStart(2, '0'); const second = String(now.getSeconds()).padStart(2, '0'); const backupDir = path.join(outputDir, `backup-${year}-${month}-${day}-${hour}-${minute}-${second}`); await ensureDir(backupDir); console.log(chalk.blue(`📁 Diretório: ${backupDir}`)); console.log(chalk.gray(`🐳 Backup via Docker Desktop`)); const manifest = { created_at: new Date().toISOString(), project_id: config.supabase.projectId, smoonb_version: require('../../package.json').version, backup_type: 'pg_dumpall_docker_dashboard_compatible', docker_version: await getDockerVersion(), dashboard_compatible: true, components: {} }; // 1. Backup Database via pg_dumpall Docker (idêntico ao Dashboard) console.log(chalk.blue('\n📊 1/8 - Backup da Database PostgreSQL via pg_dumpall Docker...')); const databaseResult = await backupDatabase(config.supabase.projectId, backupDir); manifest.components.database = databaseResult; // 1.5. Backup Database Separado (SQL files para troubleshooting) console.log(chalk.blue('\n📊 1.5/8 - Backup da Database PostgreSQL (arquivos SQL separados)...')); const dbSeparatedResult = await backupDatabaseSeparated(config.supabase.projectId, backupDir); manifest.components.database_separated = { success: dbSeparatedResult.success, method: 'supabase-cli', files: dbSeparatedResult.files || [], total_size_kb: dbSeparatedResult.totalSizeKB || '0.0' }; // 2. Backup Edge Functions via Docker console.log(chalk.blue('\n⚡ 2/8 - Backup das Edge Functions via Docker...')); const functionsResult = await backupEdgeFunctionsWithDocker(config.supabase.projectId, config.supabase.accessToken, backupDir); manifest.components.edge_functions = functionsResult; // 3. Backup Auth Settings via API console.log(chalk.blue('\n🔐 3/8 - Backup das Auth Settings via API...')); const authResult = await backupAuthSettings(config.supabase.projectId, config.supabase.accessToken, backupDir); manifest.components.auth_settings = authResult; // 4. Backup Storage via API console.log(chalk.blue('\n📦 4/8 - Backup do Storage via API...')); const storageResult = await backupStorage(config.supabase.projectId, config.supabase.accessToken, backupDir); manifest.components.storage = storageResult; // 5. Backup Custom Roles via SQL console.log(chalk.blue('\n👥 5/8 - Backup dos Custom Roles via SQL...')); const rolesResult = await backupCustomRoles(config.supabase.databaseUrl, backupDir); manifest.components.custom_roles = rolesResult; // 6. Backup das Database Extensions and Settings via SQL console.log(chalk.blue('\n🔧 6/8 - Backup das Database Extensions and Settings via SQL...')); const databaseSettingsResult = await backupDatabaseSettings(config.supabase.projectId, backupDir); manifest.components.database_settings = databaseSettingsResult; // 7. Backup Realtime Settings via Captura Interativa console.log(chalk.blue('\n🔄 7/8 - Backup das Realtime Settings via Captura Interativa...')); const realtimeResult = await backupRealtimeSettings(config.supabase.projectId, backupDir, options.skipRealtime); manifest.components.realtime = realtimeResult; // Salvar manifest await writeJson(path.join(backupDir, 'backup-manifest.json'), manifest); console.log(chalk.green('\n🎉 BACKUP COMPLETO FINALIZADO VIA DOCKER!')); console.log(chalk.blue(`📁 Localização: ${backupDir}`)); console.log(chalk.green(`📊 Database: ${databaseResult.fileName} (${databaseResult.size} KB) - Idêntico ao Dashboard`)); console.log(chalk.green(`📊 Database SQL: ${dbSeparatedResult.files?.length || 0} arquivos separados (${dbSeparatedResult.totalSizeKB} KB) - Para troubleshooting`)); console.log(chalk.green(`🔧 Database Settings: ${databaseSettingsResult.fileName} (${databaseSettingsResult.size} KB) - Extensions e Configurações`)); console.log(chalk.green(`⚡ Edge Functions: ${functionsResult.success_count || 0}/${functionsResult.functions_count || 0} functions baixadas via Docker`)); console.log(chalk.green(`🔐 Auth Settings: ${authResult.success ? 'Exportadas via API' : 'Falharam'}`)); console.log(chalk.green(`📦 Storage: ${storageResult.buckets?.length || 0} buckets verificados via API`)); console.log(chalk.green(`👥 Custom Roles: ${rolesResult.roles?.length || 0} roles exportados via SQL`)); // Determinar mensagem correta baseada no método usado let realtimeMessage = 'Falharam'; if (realtimeResult.success) { if (options.skipRealtime) { realtimeMessage = 'Configurações copiadas do backup anterior'; } else { realtimeMessage = 'Configurações capturadas interativamente'; } } console.log(chalk.green(`🔄 Realtime: ${realtimeMessage}`)); return { success: true, backupDir, manifest }; } // Função para mostrar mensagens educativas e encerrar elegantemente function showDockerMessagesAndExit(reason) { console.log(''); switch (reason) { case 'docker_not_installed': console.log(chalk.red('❌ DOCKER DESKTOP NÃO ENCONTRADO')); console.log(''); console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:')); console.log(chalk.yellow(' 1. Instalar Docker Desktop')); console.log(chalk.yellow(' 2. Executar Docker Desktop')); console.log(chalk.yellow(' 3. Repetir o comando de backup')); console.log(''); console.log(chalk.blue('🔗 Download: https://docs.docker.com/desktop/install/')); console.log(''); console.log(chalk.gray('💡 O Docker Desktop é obrigatório para backup completo do Supabase')); console.log(chalk.gray(' - Database PostgreSQL')); console.log(chalk.gray(' - Edge Functions')); console.log(chalk.gray(' - Todos os componentes via Supabase CLI')); break; case 'docker_not_running': console.log(chalk.red('❌ DOCKER DESKTOP NÃO ESTÁ EXECUTANDO')); console.log(''); console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:')); console.log(chalk.yellow(' 1. Abrir Docker Desktop')); console.log(chalk.yellow(' 2. Aguardar inicialização completa')); console.log(chalk.yellow(' 3. Repetir o comando de backup')); console.log(''); console.log(chalk.blue('💡 Dica: Docker Desktop deve estar rodando em segundo plano')); console.log(''); console.log(chalk.gray('💡 O Docker Desktop é obrigatório para backup completo do Supabase')); console.log(chalk.gray(' - Database PostgreSQL')); console.log(chalk.gray(' - Edge Functions')); console.log(chalk.gray(' - Todos os componentes via Supabase CLI')); break; case 'supabase_cli_not_found': console.log(chalk.red('❌ SUPABASE CLI NÃO ENCONTRADO')); console.log(''); console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:')); console.log(chalk.yellow(' 1. Instalar Supabase CLI')); console.log(chalk.yellow(' 2. Repetir o comando de backup')); console.log(''); console.log(chalk.blue('🔗 Instalação: npm install -g supabase')); console.log(''); console.log(chalk.gray('💡 O Supabase CLI é obrigatório para backup completo do Supabase')); console.log(chalk.gray(' - Database PostgreSQL')); console.log(chalk.gray(' - Edge Functions')); console.log(chalk.gray(' - Todos os componentes via Docker')); break; } console.log(''); console.log(chalk.red('🚫 Backup cancelado - Pré-requisitos não atendidos')); console.log(chalk.gray(' Instale os componentes necessários e tente novamente')); console.log(''); process.exit(1); } // Backup da database usando pg_dumpall via Docker (idêntico ao Supabase Dashboard) async function backupDatabase(projectId, backupDir) { try { console.log(chalk.gray(' - Criando backup completo via pg_dumpall...')); const { execSync } = require('child_process'); const config = await readConfig(); // Extrair credenciais da databaseUrl const dbUrl = config.supabase.databaseUrl; const urlMatch = dbUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/); if (!urlMatch) { throw new Error('Database URL inválida'); } const [, username, password, host, port, database] = urlMatch; // Gerar nome do arquivo igual ao dashboard const now = new Date(); const day = String(now.getDate()).padStart(2, '0'); const month = String(now.getMonth() + 1).padStart(2, '0'); const year = now.getFullYear(); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); const fileName = `db_cluster-${day}-${month}-${year}@${hours}-${minutes}-${seconds}.backup`; // CORREÇÃO: Usar caminho absoluto igual às Edge Functions const backupDirAbs = path.resolve(backupDir); // Comando pg_dumpall via Docker (mesma abordagem das Edge Functions) const dockerCmd = [ 'docker run --rm --network host', `-v "${backupDirAbs}:/host"`, `-e PGPASSWORD="${password}"`, 'postgres:17 pg_dumpall', `-h ${host}`, `-p ${port}`, `-U ${username}`, `-f /host/${fileName}` ].join(' '); console.log(chalk.gray(` - Executando pg_dumpall via Docker...`)); execSync(dockerCmd, { stdio: 'pipe' }); // Compactar igual ao Supabase Dashboard const gzipCmd = [ 'docker run --rm', `-v "${backupDirAbs}:/host"`, `postgres:17 gzip /host/${fileName}` ].join(' '); execSync(gzipCmd, { stdio: 'pipe' }); const finalFileName = `${fileName}.gz`; const stats = await fs.stat(path.join(backupDir, finalFileName)); const sizeKB = (stats.size / 1024).toFixed(1); console.log(chalk.green(` ✅ Database backup: ${finalFileName} (${sizeKB} KB)`)); return { success: true, size: sizeKB, fileName: finalFileName }; } catch (error) { console.log(chalk.yellow(` ⚠️ Erro no backup do database: ${error.message}`)); return { success: false }; } } // Backup da database usando arquivos SQL separados via Supabase CLI (para troubleshooting) async function backupDatabaseSeparated(projectId, backupDir) { try { console.log(chalk.gray(' - Criando backups SQL separados via Supabase CLI...')); const { execSync } = require('child_process'); const config = await readConfig(); const dbUrl = config.supabase.databaseUrl; const files = []; let totalSizeKB = 0; // 1. Backup do Schema console.log(chalk.gray(' - Exportando schema...')); const schemaFile = path.join(backupDir, 'schema.sql'); try { execSync(`supabase db dump --db-url "${dbUrl}" -f "${schemaFile}"`, { stdio: 'pipe' }); const stats = await fs.stat(schemaFile); const sizeKB = (stats.size / 1024).toFixed(1); files.push({ filename: 'schema.sql', sizeKB }); totalSizeKB += parseFloat(sizeKB); console.log(chalk.green(` ✅ Schema: ${sizeKB} KB`)); } catch (error) { console.log(chalk.yellow(` ⚠️ Erro no schema: ${error.message}`)); } // 2. Backup dos Dados console.log(chalk.gray(' - Exportando dados...')); const dataFile = path.join(backupDir, 'data.sql'); try { execSync(`supabase db dump --db-url "${dbUrl}" --data-only -f "${dataFile}"`, { stdio: 'pipe' }); const stats = await fs.stat(dataFile); const sizeKB = (stats.size / 1024).toFixed(1); files.push({ filename: 'data.sql', sizeKB }); totalSizeKB += parseFloat(sizeKB); console.log(chalk.green(` ✅ Data: ${sizeKB} KB`)); } catch (error) { console.log(chalk.yellow(` ⚠️ Erro nos dados: ${error.message}`)); } // 3. Backup dos Roles console.log(chalk.gray(' - Exportando roles...')); const rolesFile = path.join(backupDir, 'roles.sql'); try { execSync(`supabase db dump --db-url "${dbUrl}" --role-only -f "${rolesFile}"`, { stdio: 'pipe' }); const stats = await fs.stat(rolesFile); const sizeKB = (stats.size / 1024).toFixed(1); files.push({ filename: 'roles.sql', sizeKB }); totalSizeKB += parseFloat(sizeKB); console.log(chalk.green(` ✅ Roles: ${sizeKB} KB`)); } catch (error) { console.log(chalk.yellow(` ⚠️ Erro nos roles: ${error.message}`)); } return { success: files.length > 0, files, totalSizeKB: totalSizeKB.toFixed(1) }; } catch (error) { console.log(chalk.yellow(` ⚠️ Erro nos backups SQL separados: ${error.message}`)); return { success: false, files: [], totalSizeKB: '0.0' }; } } // Backup das Edge Functions via Docker async function backupEdgeFunctionsWithDocker(projectId, accessToken, backupDir) { try { const functionsDir = path.join(backupDir, 'edge-functions'); await ensureDir(functionsDir); console.log(chalk.gray(' - Listando Edge Functions via Management API...')); // ✅ Usar fetch direto para Management API com Personal Access Token const functionsResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/functions`, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' } }); if (!functionsResponse.ok) { console.log(chalk.yellow(` ⚠️ Erro ao listar Edge Functions: ${functionsResponse.status} ${functionsResponse.statusText}`)); return { success: false, reason: 'api_error', functions: [] }; } const functions = await functionsResponse.json(); if (!functions || functions.length === 0) { console.log(chalk.gray(' - Nenhuma Edge Function encontrada')); await writeJson(path.join(functionsDir, 'README.md'), { message: 'Nenhuma Edge Function encontrada neste projeto' }); return { success: true, reason: 'no_functions', functions: [] }; } console.log(chalk.gray(` - Encontradas ${functions.length} Edge Function(s)`)); const downloadedFunctions = []; let successCount = 0; let errorCount = 0; // ✅ Baixar cada Edge Function via Supabase CLI // Nota: O CLI ignora o cwd e sempre baixa para supabase/functions for (const func of functions) { try { console.log(chalk.gray(` - Baixando: ${func.name}...`)); // Criar diretório da função NO BACKUP const functionTargetDir = path.join(functionsDir, func.name); await ensureDir(functionTargetDir); // Diretório temporário onde o supabase CLI irá baixar (supabase/functions) const tempDownloadDir = path.join(process.cwd(), 'supabase', 'functions', func.name); // Baixar Edge Function via Supabase CLI (sempre vai para supabase/functions) const { execSync } = require('child_process'); execSync(`supabase functions download ${func.name}`, { timeout: 60000, stdio: 'pipe' }); // ✅ COPIAR arquivos de supabase/functions para o backup try { const stat = await fs.stat(tempDownloadDir); if (stat.isDirectory()) { const files = await fs.readdir(tempDownloadDir); for (const file of files) { const srcPath = path.join(tempDownloadDir, file); const dstPath = path.join(functionTargetDir, file); const fileStats = await fs.stat(srcPath); if (fileStats.isDirectory()) { // Copiar diretórios recursivamente await fs.cp(srcPath, dstPath, { recursive: true }); } else { // Copiar arquivos await fs.copyFile(srcPath, dstPath); } } } } catch (copyError) { // Arquivos não foram baixados, continuar console.log(chalk.yellow(` ⚠️ Nenhum arquivo encontrado em ${tempDownloadDir}`)); } // ✅ LIMPAR supabase/functions após copiar try { await fs.rm(tempDownloadDir, { recursive: true, force: true }); } catch (cleanError) { // Ignorar erro de limpeza } console.log(chalk.green(` ✅ ${func.name} baixada com sucesso`)); successCount++; downloadedFunctions.push({ name: func.name, slug: func.name, version: func.version || 'unknown', files: await fs.readdir(functionTargetDir).catch(() => []) }); } catch (error) { console.log(chalk.yellow(` ⚠️ Erro ao baixar ${func.name}: ${error.message}`)); errorCount++; } } console.log(chalk.green(`📊 Backup de Edge Functions concluído:`)); console.log(chalk.green(` ✅ Sucessos: ${successCount}`)); console.log(chalk.green(` ❌ Erros: ${errorCount}`)); return { success: true, reason: 'success', functions: downloadedFunctions, functions_count: functions.length, success_count: successCount, error_count: errorCount, method: 'docker' }; } catch (error) { console.log(chalk.yellow(` ⚠️ Erro durante backup de Edge Functions: ${error.message}`)); console.log('⏭️ Continuando com outros componentes...'); return { success: false, reason: 'download_error', error: error.message, functions: [] }; } } // Backup das Auth Settings via Management API async function backupAuthSettings(projectId, accessToken, backupDir) { try { console.log(chalk.gray(' - Exportando configurações de Auth via Management API...')); // ✅ Usar fetch direto para Management API com Personal Access Token const authResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/config/auth`, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' } }); if (!authResponse.ok) { console.log(chalk.yellow(` ⚠️ Erro ao obter Auth Settings: ${authResponse.status} ${authResponse.statusText}`)); return { success: false }; } const authSettings = await authResponse.json(); // Salvar configurações de Auth const authSettingsPath = path.join(backupDir, 'auth-settings.json'); await writeJson(authSettingsPath, { project_id: projectId, timestamp: new Date().toISOString(), settings: authSettings }); console.log(chalk.green(`✅ Auth Settings exportadas: ${path.basename(authSettingsPath)}`)); return { success: true }; } catch (error) { console.log(chalk.yellow(` ⚠️ Erro no backup das Auth Settings: ${error.message}`)); return { success: false }; } } // Backup do Storage via Supabase API async function backupStorage(projectId, accessToken, backupDir) { try { const storageDir = path.join(backupDir, 'storage'); await ensureDir(storageDir); console.log(chalk.gray(' - Listando buckets de Storage via Management API...')); // ✅ Usar fetch direto para Management API com Personal Access Token const storageResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/storage/buckets`, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' } }); if (!storageResponse.ok) { console.log(chalk.yellow(` ⚠️ Erro ao listar buckets: ${storageResponse.status} ${storageResponse.statusText}`)); return { success: false, buckets: [] }; } const buckets = await storageResponse.json(); if (!buckets || buckets.length === 0) { console.log(chalk.gray(' - Nenhum bucket encontrado')); await writeJson(path.join(storageDir, 'README.md'), { message: 'Nenhum bucket de Storage encontrado neste projeto' }); return { success: true, buckets: [] }; } console.log(chalk.gray(` - Encontrados ${buckets.length} buckets`)); const processedBuckets = []; for (const bucket of buckets || []) { try { console.log(chalk.gray(` - Processando bucket: ${bucket.name}`)); // ✅ Listar objetos do bucket via Management API com Personal Access Token const objectsResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/storage/buckets/${bucket.name}/objects`, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' } }); let objects = []; if (objectsResponse.ok) { objects = await objectsResponse.json(); } const bucketInfo = { id: bucket.id, name: bucket.name, public: bucket.public, file_size_limit: bucket.file_size_limit, allowed_mime_types: bucket.allowed_mime_types, objects: objects || [] }; // Salvar informações do bucket const bucketPath = path.join(storageDir, `${bucket.name}.json`); await writeJson(bucketPath, bucketInfo); processedBuckets.push({ name: bucket.name, objectCount: objects?.length || 0 }); console.log(chalk.green(` ✅ Bucket ${bucket.name}: ${objects?.length || 0} objetos`)); } catch (error) { console.log(chalk.yellow(` ⚠️ Erro ao processar bucket ${bucket.name}: ${error.message}`)); } } console.log(chalk.green(`✅ Storage backupado: ${processedBuckets.length} buckets`)); return { success: true, buckets: processedBuckets }; } catch (error) { console.log(chalk.yellow(`⚠️ Erro no backup do Storage: ${error.message}`)); return { success: false, buckets: [] }; } } // Backup dos Custom Roles via Docker async function backupCustomRoles(databaseUrl, backupDir) { try { console.log(chalk.gray(' - Exportando Custom Roles via Docker...')); const customRolesFile = path.join(backupDir, 'custom-roles.sql'); try { // ✅ Usar Supabase CLI via Docker para roles await execAsync(`supabase db dump --db-url "${databaseUrl}" --role-only -f "${customRolesFile}"`); const stats = await fs.stat(customRolesFile); const sizeKB = (stats.size / 1024).toFixed(1); console.log(chalk.green(` ✅ Custom Roles exportados via Docker: ${sizeKB} KB`)); return { success: true, roles: [{ filename: 'custom-roles.sql', sizeKB }] }; } catch (error) { console.log(chalk.yellow(` ⚠️ Erro ao exportar Custom Roles via Docker: ${error.message}`)); return { success: false, roles: [] }; } } catch (error) { console.log(chalk.yellow(` ⚠️ Erro no backup dos Custom Roles: ${error.message}`)); return { success: false, roles: [] }; } } // Backup das Database Extensions and Settings via SQL async function backupDatabaseSettings(projectId, backupDir) { try { console.log(chalk.gray(' - Capturando Database Extensions and Settings...')); const { execSync } = require('child_process'); const config = await readConfig(); // Extrair credenciais da databaseUrl const dbUrl = config.supabase.databaseUrl; const urlMatch = dbUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/); if (!urlMatch) { throw new Error('Database URL inválida'); } const [, username, password, host, port, database] = urlMatch; // Gerar nome do arquivo const now = new Date(); const day = String(now.getDate()).padStart(2, '0'); const month = String(now.getMonth() + 1).padStart(2, '0'); const year = now.getFullYear(); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); const fileName = `database-settings-${day}-${month}-${year}@${hours}-${minutes}-${seconds}.json`; // Usar caminho absoluto igual às outras funções const backupDirAbs = path.resolve(backupDir); // Script SQL para capturar todas as configurações const sqlScript = ` -- Database Extensions and Settings Backup -- Generated at: ${new Date().toISOString()} -- 1. Capturar extensões instaladas SELECT json_agg( json_build_object( 'name', extname, 'version', extversion, 'schema', extnamespace::regnamespace ) ) as extensions FROM pg_extension; -- 2. Capturar configurações PostgreSQL importantes SELECT json_agg( json_build_object( 'name', name, 'setting', setting, 'unit', unit, 'context', context, 'description', short_desc ) ) as postgres_settings FROM pg_settings WHERE name IN ( 'statement_timeout', 'idle_in_transaction_session_timeout', 'lock_timeout', 'shared_buffers', 'work_mem', 'maintenance_work_mem', 'effective_cache_size', 'max_connections', 'log_statement', 'log_min_duration_statement', 'timezone', 'log_timezone', 'default_transaction_isolation', 'default_transaction_read_only', 'checkpoint_completion_target', 'wal_buffers', 'max_wal_size', 'min_wal_size' ); -- 3. Capturar configurações específicas dos roles Supabase SELECT json_agg( json_build_object( 'role', rolname, 'config', rolconfig ) ) as role_configurations FROM pg_roles WHERE rolname IN ('anon', 'authenticated', 'authenticator', 'postgres', 'service_role') AND rolconfig IS NOT NULL; -- 4. Capturar configurações de PGAudit (se existir) SELECT json_agg( json_build_object( 'role', rolname, 'config', rolconfig ) ) as pgaudit_configurations FROM pg_roles WHERE rolconfig IS NOT NULL AND EXISTS ( SELECT 1 FROM unnest(rolconfig) AS config WHERE config LIKE '%pgaudit%' ); `; // Salvar script SQL temporário const sqlFile = path.join(backupDir, 'temp_settings.sql'); await fs.writeFile(sqlFile, sqlScript); // Executar via Docker const dockerCmd = [ 'docker run --rm --network host', `-v "${backupDirAbs}:/host"`, `-e PGPASSWORD="${password}"`, 'postgres:17 psql', `-h ${host}`, `-p ${port}`, `-U ${username}`, `-d ${database}`, '-f /host/temp_settings.sql', '-t', // Tuples only '-A' // Unaligned output ].join(' '); console.log(chalk.gray(' - Executando queries de configurações via Docker...')); const output = execSync(dockerCmd, { stdio: 'pipe', encoding: 'utf8' }); // Processar output e criar JSON estruturado const lines = output.trim().split('\n').filter(line => line.trim()); const result = { database_settings: { note: "Configurações específicas do database Supabase capturadas via SQL", captured_at: new Date().toISOString(), project_id: projectId, extensions: lines[0] ? JSON.parse(lines[0]) : [], postgres_settings: lines[1] ? JSON.parse(lines[1]) : [], role_configurations: lines[2] ? JSON.parse(lines[2]) : [], pgaudit_configurations: lines[3] ? JSON.parse(lines[3]) : [], restore_instructions: { note: "Estas configurações precisam ser aplicadas manualmente após a restauração do database", steps: [ "1. Restaurar o database usando o arquivo .backup.gz", "2. Aplicar configurações de Postgres via SQL:", " ALTER DATABASE postgres SET setting_name TO 'value';", "3. Aplicar configurações de roles via SQL:", " ALTER ROLE role_name SET setting_name TO 'value';", "4. Habilitar extensões necessárias via Dashboard ou SQL:", " CREATE EXTENSION IF NOT EXISTS extension_name;", "5. Verificar configurações aplicadas:", " SELECT name, setting FROM pg_settings WHERE name IN (...);" ] } } }; // Salvar arquivo JSON const jsonFile = path.join(backupDir, fileName); await fs.writeFile(jsonFile, JSON.stringify(result, null, 2)); // Limpar arquivo temporário await fs.unlink(sqlFile); const stats = await fs.stat(jsonFile); const sizeKB = (stats.size / 1024).toFixed(1); console.log(chalk.green(` ✅ Database Settings: ${fileName} (${sizeKB} KB)`)); return { success: true, size: sizeKB, fileName: fileName }; } catch (error) { console.log(chalk.yellow(` ⚠️ Erro no backup das Database Settings: ${error.message}`)); return { success: false }; } } // Backup das Realtime Settings via Captura Interativa async function backupRealtimeSettings(projectId, backupDir, skipInteractive = false) { try { console.log(chalk.gray(' - Capturando Realtime Settings interativamente...')); const result = await captureRealtimeSettings(projectId, backupDir, skipInteractive); const stats = await fs.stat(path.join(backupDir, 'realtime-settings.json')); const sizeKB = (stats.size / 1024).toFixed(1); console.log(chalk.green(` ✅ Realtime Settings capturadas: ${sizeKB} KB`)); return { success: true, settings: result }; } catch (error) { console.log(chalk.yellow(` ⚠️ Erro ao capturar Realtime Settings: ${error.message}`)); return { success: false }; } }