smoonb 0.0.47 → 0.0.49

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 (46) hide show
  1. package/README.md +339 -87
  2. package/bin/smoonb.js +2 -2
  3. package/package.json +1 -1
  4. package/src/commands/backup/index.js +316 -0
  5. package/src/commands/backup/steps/00-docker-validation.js +24 -0
  6. package/src/commands/backup/steps/01-database.js +72 -0
  7. package/src/commands/backup/steps/02-database-separated.js +82 -0
  8. package/src/commands/backup/steps/03-database-settings.js +178 -0
  9. package/src/commands/backup/steps/04-auth-settings.js +43 -0
  10. package/src/commands/backup/steps/05-realtime-settings.js +26 -0
  11. package/src/commands/backup/steps/06-storage.js +90 -0
  12. package/src/commands/backup/steps/07-custom-roles.js +39 -0
  13. package/src/commands/backup/steps/08-edge-functions.js +153 -0
  14. package/src/commands/backup/steps/09-supabase-temp.js +42 -0
  15. package/src/commands/backup/steps/10-migrations.js +74 -0
  16. package/src/commands/backup/utils.js +69 -0
  17. package/src/commands/check.js +0 -1
  18. package/src/commands/config.js +0 -1
  19. package/src/commands/functions.js +1 -1
  20. package/src/commands/restore/index.js +206 -0
  21. package/src/commands/restore/steps/00-backup-selection.js +38 -0
  22. package/src/commands/restore/steps/01-components-selection.js +71 -0
  23. package/src/commands/restore/steps/02-confirmation.js +14 -0
  24. package/src/commands/restore/steps/03-database.js +81 -0
  25. package/src/commands/restore/steps/04-edge-functions.js +112 -0
  26. package/src/commands/restore/steps/05-auth-settings.js +51 -0
  27. package/src/commands/restore/steps/06-storage.js +58 -0
  28. package/src/commands/restore/steps/07-database-settings.js +65 -0
  29. package/src/commands/restore/steps/08-realtime-settings.js +50 -0
  30. package/src/commands/restore/utils.js +139 -0
  31. package/src/index.js +3 -3
  32. package/src/interactive/envMapper.js +38 -14
  33. package/src/utils/cli.js +1 -1
  34. package/src/utils/config.js +1 -3
  35. package/src/utils/docker.js +3 -3
  36. package/src/utils/env.js +2 -3
  37. package/src/utils/envMap.js +1 -1
  38. package/src/utils/fsExtra.js +98 -0
  39. package/src/utils/fsx.js +2 -2
  40. package/src/utils/prompt.js +34 -0
  41. package/src/utils/realtime-settings.js +2 -2
  42. package/src/utils/supabase.js +10 -10
  43. package/src/utils/supabaseLink.js +82 -0
  44. package/src/utils/validation.js +2 -2
  45. package/src/commands/backup.js +0 -939
  46. package/src/commands/restore.js +0 -786
@@ -0,0 +1,316 @@
1
+ const chalk = require('chalk');
2
+ const path = require('path');
3
+ const fs = require('fs').promises;
4
+ const { ensureDir, writeJson } = require('../../utils/fsx');
5
+ const { readConfig, validateFor } = require('../../utils/config');
6
+ const { showBetaBanner } = require('../../utils/banner');
7
+ const { getDockerVersion } = require('../../utils/docker');
8
+ const { readEnvFile, writeEnvFile, backupEnvFile } = require('../../utils/env');
9
+ const { saveEnvMap } = require('../../utils/envMap');
10
+ const { mapEnvVariablesInteractively, askComponentsFlags } = require('../../interactive/envMapper');
11
+ const { confirm } = require('../../utils/prompt');
12
+
13
+ // Importar todas as etapas
14
+ const step00DockerValidation = require('./steps/00-docker-validation');
15
+ const step01Database = require('./steps/01-database');
16
+ const step02DatabaseSeparated = require('./steps/02-database-separated');
17
+ const step03DatabaseSettings = require('./steps/03-database-settings');
18
+ const step04AuthSettings = require('./steps/04-auth-settings');
19
+ const step05RealtimeSettings = require('./steps/05-realtime-settings');
20
+ const step06Storage = require('./steps/06-storage');
21
+ const step07CustomRoles = require('./steps/07-custom-roles');
22
+ const step08EdgeFunctions = require('./steps/08-edge-functions');
23
+ const step09SupabaseTemp = require('./steps/09-supabase-temp');
24
+ const step10Migrations = require('./steps/10-migrations');
25
+
26
+ // Exportar FUNÇÃO em vez de objeto Command
27
+ module.exports = async (options) => {
28
+ showBetaBanner();
29
+
30
+ try {
31
+ // Executar validação Docker ANTES de tudo
32
+ await step00DockerValidation();
33
+
34
+ // Consentimento para leitura e escrita do .env.local
35
+ console.log(chalk.yellow('\n⚠️ O smoonb irá ler e escrever o arquivo .env.local localmente.'));
36
+ console.log(chalk.yellow(' Um backup automático do .env.local será criado antes de qualquer alteração.'));
37
+ console.log(chalk.yellow(' Vamos mapear suas variáveis de ambiente para garantir que todas as chaves necessárias'));
38
+ console.log(chalk.yellow(' estejam presentes e com os valores corretos do projeto alvo.'));
39
+ const consentOk = await confirm('Você consente em prosseguir', true);
40
+ if (!consentOk) {
41
+ console.log(chalk.red('🚫 Operação cancelada pelo usuário.'));
42
+ process.exit(1);
43
+ }
44
+
45
+ // Carregar configuração existente apenas para defaults de diretório
46
+ const config = await readConfig().catch(() => ({ backup: { outputDir: './backups' }, supabase: {} }));
47
+ validateFor(config, 'backup');
48
+
49
+ // Pré-passo de ENV: criar diretório de backup com timestamp já no início
50
+ const now = new Date();
51
+ const year = now.getFullYear();
52
+ const month = String(now.getMonth() + 1).padStart(2, '0');
53
+ const day = String(now.getDate()).padStart(2, '0');
54
+ const hour = String(now.getHours()).padStart(2, '0');
55
+ const minute = String(now.getMinutes()).padStart(2, '0');
56
+ const second = String(now.getSeconds()).padStart(2, '0');
57
+
58
+ // Resolver diretório de saída
59
+ const defaultOutput = options.output || config.backup?.outputDir || './backups';
60
+ const backupDir = path.join(defaultOutput, `backup-${year}-${month}-${day}-${hour}-${minute}-${second}`);
61
+ await ensureDir(backupDir);
62
+
63
+ // Backup e mapeamento do .env.local
64
+ const envPath = path.join(process.cwd(), '.env.local');
65
+ const envBackupPath = path.join(backupDir, 'env', '.env.local');
66
+ await ensureDir(path.dirname(envBackupPath));
67
+ await backupEnvFile(envPath, envBackupPath);
68
+ console.log(chalk.blue(`📁 Backup do .env.local: ${path.relative(process.cwd(), envBackupPath)}`));
69
+
70
+ const expectedKeys = [
71
+ 'NEXT_PUBLIC_SUPABASE_URL',
72
+ 'NEXT_PUBLIC_SUPABASE_ANON_KEY',
73
+ 'SUPABASE_SERVICE_ROLE_KEY',
74
+ 'SUPABASE_DB_URL',
75
+ 'SUPABASE_PROJECT_ID',
76
+ 'SUPABASE_ACCESS_TOKEN',
77
+ 'SMOONB_OUTPUT_DIR'
78
+ ];
79
+ const currentEnv = await readEnvFile(envPath);
80
+ const { finalEnv, dePara } = await mapEnvVariablesInteractively(currentEnv, expectedKeys);
81
+ await writeEnvFile(envPath, finalEnv);
82
+ await saveEnvMap(dePara, path.join(backupDir, 'env', 'env-map.json'));
83
+ console.log(chalk.green('✅ .env.local atualizado com sucesso. Nenhuma chave renomeada; valores sincronizados.'));
84
+
85
+ function getValue(expectedKey) {
86
+ const clientKey = Object.keys(dePara).find(k => dePara[k] === expectedKey);
87
+ return clientKey ? finalEnv[clientKey] : '';
88
+ }
89
+
90
+ // Recalcular outputDir a partir do ENV mapeado
91
+ const resolvedOutputDir = options.output || getValue('SMOONB_OUTPUT_DIR') || config.backup?.outputDir || './backups';
92
+
93
+ // Se mudou o outputDir, movemos o backupDir inicial para o novo local mantendo timestamp
94
+ const finalBackupDir = backupDir.startsWith(path.resolve(resolvedOutputDir))
95
+ ? backupDir
96
+ : path.join(resolvedOutputDir, path.basename(backupDir));
97
+ if (finalBackupDir !== backupDir) {
98
+ await ensureDir(resolvedOutputDir);
99
+ await fs.rename(backupDir, finalBackupDir);
100
+ }
101
+
102
+ const projectId = getValue('SUPABASE_PROJECT_ID');
103
+ const accessToken = getValue('SUPABASE_ACCESS_TOKEN');
104
+ const databaseUrl = getValue('SUPABASE_DB_URL');
105
+
106
+ if (!databaseUrl) {
107
+ console.log(chalk.red('❌ DATABASE_URL NÃO CONFIGURADA'));
108
+ console.log('');
109
+ console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:'));
110
+ console.log(chalk.yellow(' 1. Configurar SUPABASE_DB_URL no .env.local'));
111
+ console.log(chalk.yellow(' 2. Repetir o comando de backup'));
112
+ console.log('');
113
+ console.log(chalk.blue('💡 Exemplo de configuração:'));
114
+ console.log(chalk.gray(' "databaseUrl": "postgresql://postgres:[senha]@db.[projeto].supabase.co:5432/postgres"'));
115
+ console.log('');
116
+ console.log(chalk.red('🚫 Backup cancelado - Configuração incompleta'));
117
+ process.exit(1);
118
+ }
119
+
120
+ if (!accessToken) {
121
+ console.log(chalk.red('❌ ACCESS_TOKEN NÃO CONFIGURADO'));
122
+ console.log('');
123
+ console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:'));
124
+ console.log(chalk.yellow(' 1. Obter Personal Access Token do Supabase'));
125
+ console.log(chalk.yellow(' 2. Configurar SUPABASE_ACCESS_TOKEN no .env.local'));
126
+ console.log(chalk.yellow(' 3. Repetir o comando de backup'));
127
+ console.log('');
128
+ console.log(chalk.blue('🔗 Como obter o token:'));
129
+ console.log(chalk.gray(' 1. Acesse: https://supabase.com/dashboard/account/tokens'));
130
+ console.log(chalk.gray(' 2. Clique: "Generate new token"'));
131
+ console.log(chalk.gray(' 3. Copie o token (formato: sbp_...)'));
132
+ console.log('');
133
+ console.log(chalk.red('🚫 Backup cancelado - Token não configurado'));
134
+ process.exit(1);
135
+ }
136
+
137
+ // Flags de componentes (perguntas interativas)
138
+ const flags = await askComponentsFlags();
139
+
140
+ // Mostrar resumo e pedir confirmação final
141
+ console.log(chalk.cyan('\n📋 RESUMO DAS CONFIGURAÇÕES:\n'));
142
+ console.log(chalk.gray(` ✅ Edge Functions: ${flags.includeFunctions ? 'Sim' : 'Não'}`));
143
+ console.log(chalk.gray(` ✅ Storage: ${flags.includeStorage ? 'Sim' : 'Não'}`));
144
+ console.log(chalk.gray(` ✅ Auth: ${flags.includeAuth ? 'Sim' : 'Não'}`));
145
+ console.log(chalk.gray(` ✅ Realtime: ${flags.includeRealtime ? 'Sim' : 'Não'}`));
146
+ console.log(chalk.gray(` 🗑️ Limpar supabase/functions após backup: ${flags.cleanFunctions ? 'Sim' : 'Não'}`));
147
+ console.log(chalk.gray(` 🗑️ Apagar supabase/.temp após backup: ${flags.cleanTemp ? 'Sim' : 'Não'}`));
148
+ console.log(chalk.gray(` 🗑️ Apagar supabase/migrations após backup: ${flags.cleanMigrations ? 'Sim' : 'Não'}`));
149
+ console.log(chalk.gray(` 📁 Diretório de backup: ${finalBackupDir}\n`));
150
+
151
+ const finalOk = await confirm('Deseja iniciar o backup com estas configurações?', true);
152
+
153
+ if (!finalOk) {
154
+ console.log(chalk.red('🚫 Operação cancelada pelo usuário.'));
155
+ process.exit(1);
156
+ }
157
+
158
+ console.log(chalk.blue(`\n🚀 Iniciando backup do projeto: ${projectId}`));
159
+
160
+ // Criar contexto compartilhado para as etapas
161
+ const context = {
162
+ projectId,
163
+ accessToken,
164
+ databaseUrl,
165
+ backupDir: finalBackupDir,
166
+ outputDir: resolvedOutputDir,
167
+ options: { ...options, flags },
168
+ cleanupFlags: {
169
+ cleanFunctions: flags.cleanFunctions || false,
170
+ cleanTemp: flags.cleanTemp || false,
171
+ cleanMigrations: flags.cleanMigrations || false
172
+ }
173
+ };
174
+
175
+ // Criar manifest
176
+ const manifest = {
177
+ created_at: new Date().toISOString(),
178
+ project_id: projectId,
179
+ smoonb_version: require('../../../package.json').version,
180
+ backup_type: 'pg_dumpall_docker_dashboard_compatible',
181
+ docker_version: await getDockerVersion(),
182
+ dashboard_compatible: true,
183
+ components: {}
184
+ };
185
+
186
+ // Executar todas as etapas na ordem
187
+ console.log(chalk.blue(`📁 Diretório: ${finalBackupDir}`));
188
+ console.log(chalk.gray(`🐳 Backup via Docker Desktop`));
189
+
190
+ // 1. Backup Database via pg_dumpall Docker
191
+ console.log(chalk.blue('\n📊 1/11 - Backup da Database PostgreSQL via pg_dumpall Docker...'));
192
+ const databaseResult = await step01Database(context);
193
+ manifest.components.database = databaseResult;
194
+
195
+ // 2. Backup Database Separado
196
+ console.log(chalk.blue('\n📊 2/11 - Backup da Database PostgreSQL (arquivos SQL separados)...'));
197
+ const dbSeparatedResult = await step02DatabaseSeparated(context);
198
+ manifest.components.database_separated = {
199
+ success: dbSeparatedResult.success,
200
+ method: 'supabase-cli',
201
+ files: dbSeparatedResult.files || [],
202
+ total_size_kb: dbSeparatedResult.totalSizeKB || '0.0'
203
+ };
204
+
205
+ // 3. Backup Database Settings
206
+ console.log(chalk.blue('\n🔧 3/11 - Backup das Database Extensions and Settings via SQL...'));
207
+ const databaseSettingsResult = await step03DatabaseSettings(context);
208
+ manifest.components.database_settings = databaseSettingsResult;
209
+
210
+ // 4. Backup Auth Settings
211
+ if (flags?.includeAuth) {
212
+ console.log(chalk.blue('\n🔐 4/11 - Backup das Auth Settings via API...'));
213
+ const authResult = await step04AuthSettings(context);
214
+ manifest.components.auth_settings = authResult;
215
+ }
216
+
217
+ // 5. Backup Realtime Settings
218
+ if (flags?.includeRealtime) {
219
+ console.log(chalk.blue('\n🔄 5/11 - Backup das Realtime Settings via Captura Interativa...'));
220
+ const realtimeResult = await step05RealtimeSettings(context);
221
+ manifest.components.realtime = realtimeResult;
222
+ }
223
+
224
+ // 6. Backup Storage
225
+ if (flags?.includeStorage) {
226
+ console.log(chalk.blue('\n📦 6/11 - Backup do Storage via API...'));
227
+ const storageResult = await step06Storage(context);
228
+ manifest.components.storage = storageResult;
229
+ }
230
+
231
+ // 7. Backup Custom Roles
232
+ console.log(chalk.blue('\n👥 7/11 - Backup dos Custom Roles via SQL...'));
233
+ const rolesResult = await step07CustomRoles(context);
234
+ manifest.components.custom_roles = rolesResult;
235
+
236
+ // 8. Backup Edge Functions
237
+ if (flags?.includeFunctions) {
238
+ console.log(chalk.blue('\n⚡ 8/11 - Backup das Edge Functions via Docker...'));
239
+ const functionsResult = await step08EdgeFunctions(context);
240
+ manifest.components.edge_functions = functionsResult;
241
+ }
242
+
243
+ // 9. Backup Supabase .temp
244
+ console.log(chalk.blue('\n📁 9/11 - Backup do Supabase .temp...'));
245
+ const supabaseTempResult = await step09SupabaseTemp(context);
246
+ manifest.components.supabase_temp = supabaseTempResult;
247
+
248
+ // 10. Backup Migrations
249
+ console.log(chalk.blue('\n📋 10/11 - Backup das Migrations...'));
250
+ const migrationsResult = await step10Migrations(context);
251
+ manifest.components.migrations = migrationsResult;
252
+
253
+ // Salvar manifest
254
+ await writeJson(path.join(finalBackupDir, 'backup-manifest.json'), manifest);
255
+
256
+ // Exibir resumo final
257
+ console.log(chalk.green('\n🎉 BACKUP COMPLETO FINALIZADO VIA DOCKER!'));
258
+ console.log(chalk.blue(`📁 Localização: ${finalBackupDir}`));
259
+ console.log(chalk.green(`📊 Database: ${databaseResult.fileName} (${databaseResult.size} KB) - Idêntico ao Dashboard`));
260
+ console.log(chalk.green(`📊 Database SQL: ${dbSeparatedResult.files?.length || 0} arquivos separados (${dbSeparatedResult.totalSizeKB} KB) - Para troubleshooting`));
261
+ console.log(chalk.green(`🔧 Database Settings: ${databaseSettingsResult.fileName} (${databaseSettingsResult.size} KB) - Extensions e Configurações`));
262
+
263
+ if (flags?.includeFunctions && manifest.components.edge_functions) {
264
+ const functionsResult = manifest.components.edge_functions;
265
+ console.log(chalk.green(`⚡ Edge Functions: ${functionsResult.success_count || 0}/${functionsResult.functions_count || 0} functions baixadas via Docker`));
266
+ }
267
+ if (flags?.includeAuth && manifest.components.auth_settings) {
268
+ const authResult = manifest.components.auth_settings;
269
+ console.log(chalk.green(`🔐 Auth Settings: ${authResult.success ? 'Exportadas via API' : 'Falharam'}`));
270
+ }
271
+ if (flags?.includeStorage && manifest.components.storage) {
272
+ const storageResult = manifest.components.storage;
273
+ console.log(chalk.green(`📦 Storage: ${storageResult.buckets?.length || 0} buckets verificados via API`));
274
+ }
275
+ console.log(chalk.green(`👥 Custom Roles: ${rolesResult.roles?.length || 0} roles exportados via SQL`));
276
+
277
+ // Determinar mensagem correta baseada no método usado
278
+ if (flags?.includeRealtime && manifest.components.realtime) {
279
+ const realtimeResult = manifest.components.realtime;
280
+ let realtimeMessage = 'Falharam';
281
+ if (realtimeResult.success) {
282
+ if (options.skipRealtime) {
283
+ realtimeMessage = 'Configurações copiadas do backup anterior';
284
+ } else {
285
+ realtimeMessage = 'Configurações capturadas interativamente';
286
+ }
287
+ }
288
+ console.log(chalk.green(`🔄 Realtime: ${realtimeMessage}`));
289
+ }
290
+
291
+ // report.json
292
+ await writeJson(path.join(finalBackupDir, 'report.json'), {
293
+ process: 'backup',
294
+ created_at: manifest.created_at,
295
+ project_id: manifest.project_id,
296
+ assets: {
297
+ env: path.join(finalBackupDir, 'env', '.env.local'),
298
+ env_map: path.join(finalBackupDir, 'env', 'env-map.json'),
299
+ manifest: path.join(finalBackupDir, 'backup-manifest.json')
300
+ },
301
+ components: {
302
+ includeFunctions: !!flags?.includeFunctions,
303
+ includeStorage: !!flags?.includeStorage,
304
+ includeAuth: !!flags?.includeAuth,
305
+ includeRealtime: !!flags?.includeRealtime
306
+ }
307
+ });
308
+
309
+ return { success: true, backupDir: finalBackupDir, manifest };
310
+
311
+ } catch (error) {
312
+ console.error(chalk.red(`❌ Erro no backup: ${error.message}`));
313
+ process.exit(1);
314
+ }
315
+ };
316
+
@@ -0,0 +1,24 @@
1
+ const chalk = require('chalk');
2
+ const { canPerformCompleteBackup } = require('../../../utils/docker');
3
+ const { showDockerMessagesAndExit } = require('../utils');
4
+
5
+ /**
6
+ * Etapa 0: Validação Docker
7
+ * Deve ocorrer antes de tudo
8
+ */
9
+ module.exports = async () => {
10
+ console.log(chalk.blue('\n🐳 Validação Docker...'));
11
+ console.log(chalk.gray('🔍 Verificando dependências Docker...'));
12
+
13
+ const backupCapability = await canPerformCompleteBackup();
14
+
15
+ if (!backupCapability.canBackupComplete) {
16
+ showDockerMessagesAndExit(backupCapability.reason);
17
+ }
18
+
19
+ console.log(chalk.green('✅ Docker Desktop detectado e funcionando'));
20
+ console.log(chalk.gray(`🐳 Versão: ${backupCapability.dockerStatus.version}`));
21
+
22
+ return { success: true };
23
+ };
24
+
@@ -0,0 +1,72 @@
1
+ const chalk = require('chalk');
2
+ const path = require('path');
3
+ const fs = require('fs').promises;
4
+ const { execSync } = require('child_process');
5
+
6
+ /**
7
+ * Etapa 1: Backup Database via pg_dumpall Docker (idêntico ao Dashboard)
8
+ */
9
+ module.exports = async ({ databaseUrl, backupDir }) => {
10
+ try {
11
+ console.log(chalk.gray(' - Criando backup completo via pg_dumpall...'));
12
+
13
+ // Extrair credenciais da databaseUrl
14
+ const urlMatch = databaseUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
15
+
16
+ if (!urlMatch) {
17
+ throw new Error('Database URL inválida');
18
+ }
19
+
20
+ const [, username, password, host, port] = urlMatch;
21
+
22
+ // Gerar nome do arquivo igual ao dashboard
23
+ const now = new Date();
24
+ const day = String(now.getDate()).padStart(2, '0');
25
+ const month = String(now.getMonth() + 1).padStart(2, '0');
26
+ const year = now.getFullYear();
27
+ const hours = String(now.getHours()).padStart(2, '0');
28
+ const minutes = String(now.getMinutes()).padStart(2, '0');
29
+ const seconds = String(now.getSeconds()).padStart(2, '0');
30
+
31
+ const fileName = `db_cluster-${day}-${month}-${year}@${hours}-${minutes}-${seconds}.backup`;
32
+
33
+ // Usar caminho absoluto igual às Edge Functions
34
+ const backupDirAbs = path.resolve(backupDir);
35
+
36
+ // Comando pg_dumpall via Docker
37
+ const dockerCmd = [
38
+ 'docker run --rm --network host',
39
+ `-v "${backupDirAbs}:/host"`,
40
+ `-e PGPASSWORD="${password}"`,
41
+ 'postgres:17 pg_dumpall',
42
+ `-h ${host}`,
43
+ `-p ${port}`,
44
+ `-U ${username}`,
45
+ `-f /host/${fileName}`
46
+ ].join(' ');
47
+
48
+ console.log(chalk.gray(' - Executando pg_dumpall via Docker...'));
49
+ execSync(dockerCmd, { stdio: 'pipe' });
50
+
51
+ // Compactar igual ao Supabase Dashboard
52
+ const gzipCmd = [
53
+ 'docker run --rm',
54
+ `-v "${backupDirAbs}:/host"`,
55
+ `postgres:17 gzip /host/${fileName}`
56
+ ].join(' ');
57
+
58
+ execSync(gzipCmd, { stdio: 'pipe' });
59
+
60
+ const finalFileName = `${fileName}.gz`;
61
+ const stats = await fs.stat(path.join(backupDir, finalFileName));
62
+ const sizeKB = (stats.size / 1024).toFixed(1);
63
+
64
+ console.log(chalk.green(` ✅ Database backup: ${finalFileName} (${sizeKB} KB)`));
65
+
66
+ return { success: true, size: sizeKB, fileName: finalFileName };
67
+ } catch (error) {
68
+ console.log(chalk.yellow(` ⚠️ Erro no backup do database: ${error.message}`));
69
+ return { success: false };
70
+ }
71
+ };
72
+
@@ -0,0 +1,82 @@
1
+ const chalk = require('chalk');
2
+ const path = require('path');
3
+ const fs = require('fs').promises;
4
+ const { execSync } = require('child_process');
5
+
6
+ /**
7
+ * Etapa 2: Backup Database Separado (SQL files para troubleshooting)
8
+ */
9
+ module.exports = async ({ databaseUrl, backupDir, accessToken }) => {
10
+ try {
11
+ console.log(chalk.gray(' - Criando backups SQL separados via Supabase CLI...'));
12
+
13
+ const dbUrl = databaseUrl;
14
+ const files = [];
15
+ let totalSizeKB = 0;
16
+
17
+ // 1. Backup do Schema
18
+ console.log(chalk.gray(' - Exportando schema...'));
19
+ const schemaFile = path.join(backupDir, 'schema.sql');
20
+
21
+ try {
22
+ execSync(`supabase db dump --db-url "${dbUrl}" -f "${schemaFile}"`, {
23
+ stdio: 'pipe',
24
+ env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken || '' }
25
+ });
26
+ const stats = await fs.stat(schemaFile);
27
+ const sizeKB = (stats.size / 1024).toFixed(1);
28
+ files.push({ filename: 'schema.sql', sizeKB });
29
+ totalSizeKB += parseFloat(sizeKB);
30
+ console.log(chalk.green(` ✅ Schema: ${sizeKB} KB`));
31
+ } catch (error) {
32
+ console.log(chalk.yellow(` ⚠️ Erro no schema: ${error.message}`));
33
+ }
34
+
35
+ // 2. Backup dos Dados
36
+ console.log(chalk.gray(' - Exportando dados...'));
37
+ const dataFile = path.join(backupDir, 'data.sql');
38
+
39
+ try {
40
+ execSync(`supabase db dump --db-url "${dbUrl}" --data-only -f "${dataFile}"`, {
41
+ stdio: 'pipe',
42
+ env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken || '' }
43
+ });
44
+ const stats = await fs.stat(dataFile);
45
+ const sizeKB = (stats.size / 1024).toFixed(1);
46
+ files.push({ filename: 'data.sql', sizeKB });
47
+ totalSizeKB += parseFloat(sizeKB);
48
+ console.log(chalk.green(` ✅ Data: ${sizeKB} KB`));
49
+ } catch (error) {
50
+ console.log(chalk.yellow(` ⚠️ Erro nos dados: ${error.message}`));
51
+ }
52
+
53
+ // 3. Backup dos Roles
54
+ console.log(chalk.gray(' - Exportando roles...'));
55
+ const rolesFile = path.join(backupDir, 'roles.sql');
56
+
57
+ try {
58
+ execSync(`supabase db dump --db-url "${dbUrl}" --role-only -f "${rolesFile}"`, {
59
+ stdio: 'pipe',
60
+ env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken || '' }
61
+ });
62
+ const stats = await fs.stat(rolesFile);
63
+ const sizeKB = (stats.size / 1024).toFixed(1);
64
+ files.push({ filename: 'roles.sql', sizeKB });
65
+ totalSizeKB += parseFloat(sizeKB);
66
+ console.log(chalk.green(` ✅ Roles: ${sizeKB} KB`));
67
+ } catch (error) {
68
+ console.log(chalk.yellow(` ⚠️ Erro nos roles: ${error.message}`));
69
+ }
70
+
71
+ return {
72
+ success: files.length > 0,
73
+ files,
74
+ totalSizeKB: totalSizeKB.toFixed(1)
75
+ };
76
+
77
+ } catch (error) {
78
+ console.log(chalk.yellow(` ⚠️ Erro nos backups SQL separados: ${error.message}`));
79
+ return { success: false, files: [], totalSizeKB: '0.0' };
80
+ }
81
+ };
82
+
@@ -0,0 +1,178 @@
1
+ const chalk = require('chalk');
2
+ const path = require('path');
3
+ const fs = require('fs').promises;
4
+ const { execSync } = require('child_process');
5
+
6
+ /**
7
+ * Etapa 3: Backup Database Extensions and Settings via SQL
8
+ */
9
+ module.exports = async ({ databaseUrl, projectId, backupDir }) => {
10
+ try {
11
+ console.log(chalk.gray(' - Capturando Database Extensions and Settings...'));
12
+
13
+ // Extrair credenciais da databaseUrl
14
+ const urlMatch = databaseUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
15
+
16
+ if (!urlMatch) {
17
+ throw new Error('Database URL inválida');
18
+ }
19
+
20
+ const [, username, password, host, port, database] = urlMatch;
21
+
22
+ // Gerar nome do arquivo
23
+ const now = new Date();
24
+ const day = String(now.getDate()).padStart(2, '0');
25
+ const month = String(now.getMonth() + 1).padStart(2, '0');
26
+ const year = now.getFullYear();
27
+ const hours = String(now.getHours()).padStart(2, '0');
28
+ const minutes = String(now.getMinutes()).padStart(2, '0');
29
+ const seconds = String(now.getSeconds()).padStart(2, '0');
30
+
31
+ const fileName = `database-settings-${day}-${month}-${year}@${hours}-${minutes}-${seconds}.json`;
32
+
33
+ // Usar caminho absoluto igual às outras funções
34
+ const backupDirAbs = path.resolve(backupDir);
35
+
36
+ // Script SQL para capturar todas as configurações
37
+ const sqlScript = `
38
+ -- Database Extensions and Settings Backup
39
+ -- Generated at: ${new Date().toISOString()}
40
+
41
+ -- 1. Capturar extensões instaladas
42
+ SELECT json_agg(
43
+ json_build_object(
44
+ 'name', extname,
45
+ 'version', extversion,
46
+ 'schema', extnamespace::regnamespace
47
+ )
48
+ ) as extensions
49
+ FROM pg_extension;
50
+
51
+ -- 2. Capturar configurações PostgreSQL importantes
52
+ SELECT json_agg(
53
+ json_build_object(
54
+ 'name', name,
55
+ 'setting', setting,
56
+ 'unit', unit,
57
+ 'context', context,
58
+ 'description', short_desc
59
+ )
60
+ ) as postgres_settings
61
+ FROM pg_settings
62
+ WHERE name IN (
63
+ 'statement_timeout',
64
+ 'idle_in_transaction_session_timeout',
65
+ 'lock_timeout',
66
+ 'shared_buffers',
67
+ 'work_mem',
68
+ 'maintenance_work_mem',
69
+ 'effective_cache_size',
70
+ 'max_connections',
71
+ 'log_statement',
72
+ 'log_min_duration_statement',
73
+ 'timezone',
74
+ 'log_timezone',
75
+ 'default_transaction_isolation',
76
+ 'default_transaction_read_only',
77
+ 'checkpoint_completion_target',
78
+ 'wal_buffers',
79
+ 'max_wal_size',
80
+ 'min_wal_size'
81
+ );
82
+
83
+ -- 3. Capturar configurações específicas dos roles Supabase
84
+ SELECT json_agg(
85
+ json_build_object(
86
+ 'role', rolname,
87
+ 'config', rolconfig
88
+ )
89
+ ) as role_configurations
90
+ FROM pg_roles
91
+ WHERE rolname IN ('anon', 'authenticated', 'authenticator', 'postgres', 'service_role')
92
+ AND rolconfig IS NOT NULL;
93
+
94
+ -- 4. Capturar configurações de PGAudit (se existir)
95
+ SELECT json_agg(
96
+ json_build_object(
97
+ 'role', rolname,
98
+ 'config', rolconfig
99
+ )
100
+ ) as pgaudit_configurations
101
+ FROM pg_roles
102
+ WHERE rolconfig IS NOT NULL
103
+ AND EXISTS (
104
+ SELECT 1 FROM unnest(rolconfig) AS config
105
+ WHERE config LIKE '%pgaudit%'
106
+ );
107
+ `;
108
+
109
+ // Salvar script SQL temporário
110
+ const sqlFile = path.join(backupDir, 'temp_settings.sql');
111
+ await fs.writeFile(sqlFile, sqlScript);
112
+
113
+ // Executar via Docker
114
+ const dockerCmd = [
115
+ 'docker run --rm --network host',
116
+ `-v "${backupDirAbs}:/host"`,
117
+ `-e PGPASSWORD="${password}"`,
118
+ 'postgres:17 psql',
119
+ `-h ${host}`,
120
+ `-p ${port}`,
121
+ `-U ${username}`,
122
+ `-d ${database}`,
123
+ '-f /host/temp_settings.sql',
124
+ '-t', // Tuples only
125
+ '-A' // Unaligned output
126
+ ].join(' ');
127
+
128
+ console.log(chalk.gray(' - Executando queries de configurações via Docker...'));
129
+ const output = execSync(dockerCmd, { stdio: 'pipe', encoding: 'utf8' });
130
+
131
+ // Processar output e criar JSON estruturado
132
+ const lines = output.trim().split('\n').filter(line => line.trim());
133
+
134
+ const result = {
135
+ database_settings: {
136
+ note: "Configurações específicas do database Supabase capturadas via SQL",
137
+ captured_at: new Date().toISOString(),
138
+ project_id: projectId,
139
+ extensions: lines[0] ? JSON.parse(lines[0]) : [],
140
+ postgres_settings: lines[1] ? JSON.parse(lines[1]) : [],
141
+ role_configurations: lines[2] ? JSON.parse(lines[2]) : [],
142
+ pgaudit_configurations: lines[3] ? JSON.parse(lines[3]) : [],
143
+ restore_instructions: {
144
+ note: "Estas configurações precisam ser aplicadas manualmente após a restauração do database",
145
+ steps: [
146
+ "1. Restaurar o database usando o arquivo .backup.gz",
147
+ "2. Aplicar configurações de Postgres via SQL:",
148
+ " ALTER DATABASE postgres SET setting_name TO 'value';",
149
+ "3. Aplicar configurações de roles via SQL:",
150
+ " ALTER ROLE role_name SET setting_name TO 'value';",
151
+ "4. Habilitar extensões necessárias via Dashboard ou SQL:",
152
+ " CREATE EXTENSION IF NOT EXISTS extension_name;",
153
+ "5. Verificar configurações aplicadas:",
154
+ " SELECT name, setting FROM pg_settings WHERE name IN (...);"
155
+ ]
156
+ }
157
+ }
158
+ };
159
+
160
+ // Salvar arquivo JSON
161
+ const jsonFile = path.join(backupDir, fileName);
162
+ await fs.writeFile(jsonFile, JSON.stringify(result, null, 2));
163
+
164
+ // Limpar arquivo temporário
165
+ await fs.unlink(sqlFile);
166
+
167
+ const stats = await fs.stat(jsonFile);
168
+ const sizeKB = (stats.size / 1024).toFixed(1);
169
+
170
+ console.log(chalk.green(` ✅ Database Settings: ${fileName} (${sizeKB} KB)`));
171
+
172
+ return { success: true, size: sizeKB, fileName: fileName };
173
+ } catch (error) {
174
+ console.log(chalk.yellow(` ⚠️ Erro no backup das Database Settings: ${error.message}`));
175
+ return { success: false };
176
+ }
177
+ };
178
+