smoonb 0.0.46 → 0.0.48

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 (30) hide show
  1. package/package.json +1 -1
  2. package/src/commands/backup/index.js +290 -0
  3. package/src/commands/backup/steps/00-docker-validation.js +24 -0
  4. package/src/commands/backup/steps/01-database.js +72 -0
  5. package/src/commands/backup/steps/02-database-separated.js +82 -0
  6. package/src/commands/backup/steps/03-database-settings.js +178 -0
  7. package/src/commands/backup/steps/04-auth-settings.js +43 -0
  8. package/src/commands/backup/steps/05-realtime-settings.js +26 -0
  9. package/src/commands/backup/steps/06-storage.js +90 -0
  10. package/src/commands/backup/steps/07-custom-roles.js +39 -0
  11. package/src/commands/backup/steps/08-edge-functions.js +159 -0
  12. package/src/commands/backup/steps/09-supabase-temp.js +48 -0
  13. package/src/commands/backup/steps/10-migrations.js +80 -0
  14. package/src/commands/backup/utils.js +69 -0
  15. package/src/commands/restore/index.js +190 -0
  16. package/src/commands/restore/steps/00-backup-selection.js +38 -0
  17. package/src/commands/restore/steps/01-components-selection.js +84 -0
  18. package/src/commands/restore/steps/02-confirmation.js +19 -0
  19. package/src/commands/restore/steps/03-database.js +81 -0
  20. package/src/commands/restore/steps/04-edge-functions.js +112 -0
  21. package/src/commands/restore/steps/05-auth-settings.js +51 -0
  22. package/src/commands/restore/steps/06-storage.js +58 -0
  23. package/src/commands/restore/steps/07-database-settings.js +65 -0
  24. package/src/commands/restore/steps/08-realtime-settings.js +50 -0
  25. package/src/commands/restore/utils.js +139 -0
  26. package/src/interactive/envMapper.js +37 -23
  27. package/src/utils/fsExtra.js +98 -0
  28. package/src/utils/supabaseLink.js +82 -0
  29. package/src/commands/backup.js +0 -939
  30. package/src/commands/restore.js +0 -786
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoonb",
3
- "version": "0.0.46",
3
+ "version": "0.0.48",
4
4
  "description": "Complete Supabase backup and migration tool - EXPERIMENTAL VERSION - USE AT YOUR OWN RISK",
5
5
  "preferGlobal": false,
6
6
  "preventGlobalInstall": true,
@@ -0,0 +1,290 @@
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
+
12
+ // Importar todas as etapas
13
+ const step00DockerValidation = require('./steps/00-docker-validation');
14
+ const step01Database = require('./steps/01-database');
15
+ const step02DatabaseSeparated = require('./steps/02-database-separated');
16
+ const step03DatabaseSettings = require('./steps/03-database-settings');
17
+ const step04AuthSettings = require('./steps/04-auth-settings');
18
+ const step05RealtimeSettings = require('./steps/05-realtime-settings');
19
+ const step06Storage = require('./steps/06-storage');
20
+ const step07CustomRoles = require('./steps/07-custom-roles');
21
+ const step08EdgeFunctions = require('./steps/08-edge-functions');
22
+ const step09SupabaseTemp = require('./steps/09-supabase-temp');
23
+ const step10Migrations = require('./steps/10-migrations');
24
+
25
+ // Exportar FUNÇÃO em vez de objeto Command
26
+ module.exports = async (options) => {
27
+ showBetaBanner();
28
+
29
+ try {
30
+ // Consentimento para leitura e escrita do .env.local
31
+ console.log(chalk.yellow('⚠️ O smoonb irá ler e escrever o arquivo .env.local localmente.'));
32
+ console.log(chalk.yellow(' Um backup automático do .env.local será criado antes de qualquer alteração.'));
33
+ const consent = await require('inquirer').prompt([{ type: 'confirm', name: 'ok', message: 'Você consente em prosseguir (S/n):', default: true }]);
34
+ if (!consent.ok) {
35
+ console.log(chalk.red('🚫 Operação cancelada pelo usuário.'));
36
+ process.exit(1);
37
+ }
38
+
39
+ // Carregar configuração existente apenas para defaults de diretório
40
+ const config = await readConfig().catch(() => ({ backup: { outputDir: './backups' }, supabase: {} }));
41
+ validateFor(config, 'backup');
42
+
43
+ // Pré-passo de ENV: criar diretório de backup com timestamp já no início
44
+ const now = new Date();
45
+ const year = now.getFullYear();
46
+ const month = String(now.getMonth() + 1).padStart(2, '0');
47
+ const day = String(now.getDate()).padStart(2, '0');
48
+ const hour = String(now.getHours()).padStart(2, '0');
49
+ const minute = String(now.getMinutes()).padStart(2, '0');
50
+ const second = String(now.getSeconds()).padStart(2, '0');
51
+
52
+ // Resolver diretório de saída
53
+ const defaultOutput = options.output || config.backup?.outputDir || './backups';
54
+ const backupDir = path.join(defaultOutput, `backup-${year}-${month}-${day}-${hour}-${minute}-${second}`);
55
+ await ensureDir(backupDir);
56
+
57
+ // Backup e mapeamento do .env.local
58
+ const envPath = path.join(process.cwd(), '.env.local');
59
+ const envBackupPath = path.join(backupDir, 'env', '.env.local');
60
+ await ensureDir(path.dirname(envBackupPath));
61
+ await backupEnvFile(envPath, envBackupPath);
62
+ console.log(chalk.blue(`📁 Backup do .env.local: ${path.relative(process.cwd(), envBackupPath)}`));
63
+
64
+ const expectedKeys = [
65
+ 'NEXT_PUBLIC_SUPABASE_URL',
66
+ 'NEXT_PUBLIC_SUPABASE_ANON_KEY',
67
+ 'SUPABASE_SERVICE_ROLE_KEY',
68
+ 'SUPABASE_DB_URL',
69
+ 'SUPABASE_PROJECT_ID',
70
+ 'SUPABASE_ACCESS_TOKEN',
71
+ 'SMOONB_OUTPUT_DIR'
72
+ ];
73
+ const currentEnv = await readEnvFile(envPath);
74
+ const { finalEnv, dePara } = await mapEnvVariablesInteractively(currentEnv, expectedKeys);
75
+ await writeEnvFile(envPath, finalEnv);
76
+ await saveEnvMap(dePara, path.join(backupDir, 'env', 'env-map.json'));
77
+ console.log(chalk.green('✅ .env.local atualizado com sucesso. Nenhuma chave renomeada; valores sincronizados.'));
78
+
79
+ function getValue(expectedKey) {
80
+ const clientKey = Object.keys(dePara).find(k => dePara[k] === expectedKey);
81
+ return clientKey ? finalEnv[clientKey] : '';
82
+ }
83
+
84
+ // Recalcular outputDir a partir do ENV mapeado
85
+ const resolvedOutputDir = options.output || getValue('SMOONB_OUTPUT_DIR') || config.backup?.outputDir || './backups';
86
+
87
+ // Se mudou o outputDir, movemos o backupDir inicial para o novo local mantendo timestamp
88
+ const finalBackupDir = backupDir.startsWith(path.resolve(resolvedOutputDir))
89
+ ? backupDir
90
+ : path.join(resolvedOutputDir, path.basename(backupDir));
91
+ if (finalBackupDir !== backupDir) {
92
+ await ensureDir(resolvedOutputDir);
93
+ await fs.rename(backupDir, finalBackupDir);
94
+ }
95
+
96
+ const projectId = getValue('SUPABASE_PROJECT_ID');
97
+ const accessToken = getValue('SUPABASE_ACCESS_TOKEN');
98
+ const databaseUrl = getValue('SUPABASE_DB_URL');
99
+
100
+ if (!databaseUrl) {
101
+ console.log(chalk.red('❌ DATABASE_URL NÃO CONFIGURADA'));
102
+ console.log('');
103
+ console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:'));
104
+ console.log(chalk.yellow(' 1. Configurar SUPABASE_DB_URL no .env.local'));
105
+ console.log(chalk.yellow(' 2. Repetir o comando de backup'));
106
+ console.log('');
107
+ console.log(chalk.blue('💡 Exemplo de configuração:'));
108
+ console.log(chalk.gray(' "databaseUrl": "postgresql://postgres:[senha]@db.[projeto].supabase.co:5432/postgres"'));
109
+ console.log('');
110
+ console.log(chalk.red('🚫 Backup cancelado - Configuração incompleta'));
111
+ process.exit(1);
112
+ }
113
+
114
+ if (!accessToken) {
115
+ console.log(chalk.red('❌ ACCESS_TOKEN NÃO CONFIGURADO'));
116
+ console.log('');
117
+ console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:'));
118
+ console.log(chalk.yellow(' 1. Obter Personal Access Token do Supabase'));
119
+ console.log(chalk.yellow(' 2. Configurar SUPABASE_ACCESS_TOKEN no .env.local'));
120
+ console.log(chalk.yellow(' 3. Repetir o comando de backup'));
121
+ console.log('');
122
+ console.log(chalk.blue('🔗 Como obter o token:'));
123
+ console.log(chalk.gray(' 1. Acesse: https://supabase.com/dashboard/account/tokens'));
124
+ console.log(chalk.gray(' 2. Clique: "Generate new token"'));
125
+ console.log(chalk.gray(' 3. Copie o token (formato: sbp_...)'));
126
+ console.log('');
127
+ console.log(chalk.red('🚫 Backup cancelado - Token não configurado'));
128
+ process.exit(1);
129
+ }
130
+
131
+ console.log(chalk.blue(`🚀 Iniciando backup do projeto: ${projectId}`));
132
+
133
+ // Executar validação Docker (etapa 0)
134
+ await step00DockerValidation();
135
+
136
+ // Flags de componentes
137
+ const flags = await askComponentsFlags();
138
+
139
+ // Criar contexto compartilhado para as etapas
140
+ const context = {
141
+ projectId,
142
+ accessToken,
143
+ databaseUrl,
144
+ backupDir: finalBackupDir,
145
+ outputDir: resolvedOutputDir,
146
+ options: { ...options, flags }
147
+ };
148
+
149
+ // Criar manifest
150
+ const manifest = {
151
+ created_at: new Date().toISOString(),
152
+ project_id: projectId,
153
+ smoonb_version: require('../../../package.json').version,
154
+ backup_type: 'pg_dumpall_docker_dashboard_compatible',
155
+ docker_version: await getDockerVersion(),
156
+ dashboard_compatible: true,
157
+ components: {}
158
+ };
159
+
160
+ // Executar todas as etapas na ordem
161
+ console.log(chalk.blue(`📁 Diretório: ${finalBackupDir}`));
162
+ console.log(chalk.gray(`🐳 Backup via Docker Desktop`));
163
+
164
+ // 1. Backup Database via pg_dumpall Docker
165
+ console.log(chalk.blue('\n📊 1/12 - Backup da Database PostgreSQL via pg_dumpall Docker...'));
166
+ const databaseResult = await step01Database(context);
167
+ manifest.components.database = databaseResult;
168
+
169
+ // 2. Backup Database Separado
170
+ console.log(chalk.blue('\n📊 2/12 - Backup da Database PostgreSQL (arquivos SQL separados)...'));
171
+ const dbSeparatedResult = await step02DatabaseSeparated(context);
172
+ manifest.components.database_separated = {
173
+ success: dbSeparatedResult.success,
174
+ method: 'supabase-cli',
175
+ files: dbSeparatedResult.files || [],
176
+ total_size_kb: dbSeparatedResult.totalSizeKB || '0.0'
177
+ };
178
+
179
+ // 3. Backup Database Settings
180
+ console.log(chalk.blue('\n🔧 3/12 - Backup das Database Extensions and Settings via SQL...'));
181
+ const databaseSettingsResult = await step03DatabaseSettings(context);
182
+ manifest.components.database_settings = databaseSettingsResult;
183
+
184
+ // 4. Backup Auth Settings
185
+ if (flags?.includeAuth) {
186
+ console.log(chalk.blue('\n🔐 4/12 - Backup das Auth Settings via API...'));
187
+ const authResult = await step04AuthSettings(context);
188
+ manifest.components.auth_settings = authResult;
189
+ }
190
+
191
+ // 5. Backup Realtime Settings
192
+ if (flags?.includeRealtime) {
193
+ console.log(chalk.blue('\n🔄 5/12 - Backup das Realtime Settings via Captura Interativa...'));
194
+ const realtimeResult = await step05RealtimeSettings(context);
195
+ manifest.components.realtime = realtimeResult;
196
+ }
197
+
198
+ // 6. Backup Storage
199
+ if (flags?.includeStorage) {
200
+ console.log(chalk.blue('\n📦 6/12 - Backup do Storage via API...'));
201
+ const storageResult = await step06Storage(context);
202
+ manifest.components.storage = storageResult;
203
+ }
204
+
205
+ // 7. Backup Custom Roles
206
+ console.log(chalk.blue('\n👥 7/12 - Backup dos Custom Roles via SQL...'));
207
+ const rolesResult = await step07CustomRoles(context);
208
+ manifest.components.custom_roles = rolesResult;
209
+
210
+ // 8. Backup Edge Functions
211
+ if (flags?.includeFunctions) {
212
+ console.log(chalk.blue('\n⚡ 8/12 - Backup das Edge Functions via Docker...'));
213
+ const functionsResult = await step08EdgeFunctions(context);
214
+ manifest.components.edge_functions = functionsResult;
215
+ }
216
+
217
+ // 9. Backup Supabase .temp
218
+ console.log(chalk.blue('\n📁 9/12 - Backup do Supabase .temp...'));
219
+ const supabaseTempResult = await step09SupabaseTemp(context);
220
+ manifest.components.supabase_temp = supabaseTempResult;
221
+
222
+ // 10. Backup Migrations
223
+ console.log(chalk.blue('\n📋 10/12 - Backup das Migrations...'));
224
+ const migrationsResult = await step10Migrations(context);
225
+ manifest.components.migrations = migrationsResult;
226
+
227
+ // Salvar manifest
228
+ await writeJson(path.join(finalBackupDir, 'backup-manifest.json'), manifest);
229
+
230
+ // Exibir resumo final
231
+ console.log(chalk.green('\n🎉 BACKUP COMPLETO FINALIZADO VIA DOCKER!'));
232
+ console.log(chalk.blue(`📁 Localização: ${finalBackupDir}`));
233
+ console.log(chalk.green(`📊 Database: ${databaseResult.fileName} (${databaseResult.size} KB) - Idêntico ao Dashboard`));
234
+ console.log(chalk.green(`📊 Database SQL: ${dbSeparatedResult.files?.length || 0} arquivos separados (${dbSeparatedResult.totalSizeKB} KB) - Para troubleshooting`));
235
+ console.log(chalk.green(`🔧 Database Settings: ${databaseSettingsResult.fileName} (${databaseSettingsResult.size} KB) - Extensions e Configurações`));
236
+
237
+ if (flags?.includeFunctions && manifest.components.edge_functions) {
238
+ const functionsResult = manifest.components.edge_functions;
239
+ console.log(chalk.green(`⚡ Edge Functions: ${functionsResult.success_count || 0}/${functionsResult.functions_count || 0} functions baixadas via Docker`));
240
+ }
241
+ if (flags?.includeAuth && manifest.components.auth_settings) {
242
+ const authResult = manifest.components.auth_settings;
243
+ console.log(chalk.green(`🔐 Auth Settings: ${authResult.success ? 'Exportadas via API' : 'Falharam'}`));
244
+ }
245
+ if (flags?.includeStorage && manifest.components.storage) {
246
+ const storageResult = manifest.components.storage;
247
+ console.log(chalk.green(`📦 Storage: ${storageResult.buckets?.length || 0} buckets verificados via API`));
248
+ }
249
+ console.log(chalk.green(`👥 Custom Roles: ${rolesResult.roles?.length || 0} roles exportados via SQL`));
250
+
251
+ // Determinar mensagem correta baseada no método usado
252
+ if (flags?.includeRealtime && manifest.components.realtime) {
253
+ const realtimeResult = manifest.components.realtime;
254
+ let realtimeMessage = 'Falharam';
255
+ if (realtimeResult.success) {
256
+ if (options.skipRealtime) {
257
+ realtimeMessage = 'Configurações copiadas do backup anterior';
258
+ } else {
259
+ realtimeMessage = 'Configurações capturadas interativamente';
260
+ }
261
+ }
262
+ console.log(chalk.green(`🔄 Realtime: ${realtimeMessage}`));
263
+ }
264
+
265
+ // report.json
266
+ await writeJson(path.join(finalBackupDir, 'report.json'), {
267
+ process: 'backup',
268
+ created_at: manifest.created_at,
269
+ project_id: manifest.project_id,
270
+ assets: {
271
+ env: path.join(finalBackupDir, 'env', '.env.local'),
272
+ env_map: path.join(finalBackupDir, 'env', 'env-map.json'),
273
+ manifest: path.join(finalBackupDir, 'backup-manifest.json')
274
+ },
275
+ components: {
276
+ includeFunctions: !!flags?.includeFunctions,
277
+ includeStorage: !!flags?.includeStorage,
278
+ includeAuth: !!flags?.includeAuth,
279
+ includeRealtime: !!flags?.includeRealtime
280
+ }
281
+ });
282
+
283
+ return { success: true, backupDir: finalBackupDir, manifest };
284
+
285
+ } catch (error) {
286
+ console.error(chalk.red(`❌ Erro no backup: ${error.message}`));
287
+ process.exit(1);
288
+ }
289
+ };
290
+
@@ -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🐳 0/12 - 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
+