smoonb 0.0.47 → 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.
- package/package.json +1 -1
- package/src/commands/backup/index.js +290 -0
- package/src/commands/backup/steps/00-docker-validation.js +24 -0
- package/src/commands/backup/steps/01-database.js +72 -0
- package/src/commands/backup/steps/02-database-separated.js +82 -0
- package/src/commands/backup/steps/03-database-settings.js +178 -0
- package/src/commands/backup/steps/04-auth-settings.js +43 -0
- package/src/commands/backup/steps/05-realtime-settings.js +26 -0
- package/src/commands/backup/steps/06-storage.js +90 -0
- package/src/commands/backup/steps/07-custom-roles.js +39 -0
- package/src/commands/backup/steps/08-edge-functions.js +159 -0
- package/src/commands/backup/steps/09-supabase-temp.js +48 -0
- package/src/commands/backup/steps/10-migrations.js +80 -0
- package/src/commands/backup/utils.js +69 -0
- package/src/commands/restore/index.js +190 -0
- package/src/commands/restore/steps/00-backup-selection.js +38 -0
- package/src/commands/restore/steps/01-components-selection.js +84 -0
- package/src/commands/restore/steps/02-confirmation.js +19 -0
- package/src/commands/restore/steps/03-database.js +81 -0
- package/src/commands/restore/steps/04-edge-functions.js +112 -0
- package/src/commands/restore/steps/05-auth-settings.js +51 -0
- package/src/commands/restore/steps/06-storage.js +58 -0
- package/src/commands/restore/steps/07-database-settings.js +65 -0
- package/src/commands/restore/steps/08-realtime-settings.js +50 -0
- package/src/commands/restore/utils.js +139 -0
- package/src/utils/fsExtra.js +98 -0
- package/src/utils/supabaseLink.js +82 -0
- package/src/commands/backup.js +0 -939
- package/src/commands/restore.js +0 -786
package/src/commands/backup.js
DELETED
|
@@ -1,939 +0,0 @@
|
|
|
1
|
-
const chalk = require('chalk');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const fs = require('fs').promises;
|
|
4
|
-
const { exec } = require('child_process');
|
|
5
|
-
const { promisify } = require('util');
|
|
6
|
-
const { ensureDir, writeJson, copyDir } = require('../utils/fsx');
|
|
7
|
-
const { sha256 } = require('../utils/hash');
|
|
8
|
-
const { readConfig, validateFor } = require('../utils/config');
|
|
9
|
-
const { showBetaBanner } = require('../utils/banner');
|
|
10
|
-
const { canPerformCompleteBackup, getDockerVersion } = require('../utils/docker');
|
|
11
|
-
const { captureRealtimeSettings } = require('../utils/realtime-settings');
|
|
12
|
-
const { readEnvFile, writeEnvFile, backupEnvFile } = require('../utils/env');
|
|
13
|
-
const { saveEnvMap } = require('../utils/envMap');
|
|
14
|
-
const { mapEnvVariablesInteractively, askComponentsFlags } = require('../interactive/envMapper');
|
|
15
|
-
|
|
16
|
-
const execAsync = promisify(exec);
|
|
17
|
-
|
|
18
|
-
// Exportar FUNÇÃO em vez de objeto Command
|
|
19
|
-
module.exports = async (options) => {
|
|
20
|
-
showBetaBanner();
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
// Consentimento para leitura e escrita do .env.local
|
|
24
|
-
console.log(chalk.yellow('⚠️ O smoonb irá ler e escrever o arquivo .env.local localmente.'));
|
|
25
|
-
console.log(chalk.yellow(' Um backup automático do .env.local será criado antes de qualquer alteração.'));
|
|
26
|
-
const consent = await require('inquirer').prompt([{ type: 'confirm', name: 'ok', message: 'Você consente em prosseguir (S/n):', default: true }]);
|
|
27
|
-
if (!consent.ok) {
|
|
28
|
-
console.log(chalk.red('🚫 Operação cancelada pelo usuário.'));
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Carregar configuração existente apenas para defaults de diretório
|
|
33
|
-
const config = await readConfig().catch(() => ({ backup: { outputDir: './backups' }, supabase: {} }));
|
|
34
|
-
validateFor(config, 'backup');
|
|
35
|
-
|
|
36
|
-
// Validação adicional para pré-requisitos obrigatórios
|
|
37
|
-
// Pré-passo de ENV: criar diretório de backup com timestamp já no início
|
|
38
|
-
const now = new Date();
|
|
39
|
-
const year = now.getFullYear();
|
|
40
|
-
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
41
|
-
const day = String(now.getDate()).padStart(2, '0');
|
|
42
|
-
const hour = String(now.getHours()).padStart(2, '0');
|
|
43
|
-
const minute = String(now.getMinutes()).padStart(2, '0');
|
|
44
|
-
const second = String(now.getSeconds()).padStart(2, '0');
|
|
45
|
-
|
|
46
|
-
// Resolver diretório de saída (prioriza .env.local mapeado depois, por ora usa default)
|
|
47
|
-
const defaultOutput = options.output || config.backup?.outputDir || './backups';
|
|
48
|
-
const backupDir = path.join(defaultOutput, `backup-${year}-${month}-${day}-${hour}-${minute}-${second}`);
|
|
49
|
-
await ensureDir(backupDir);
|
|
50
|
-
|
|
51
|
-
// Backup e mapeamento do .env.local
|
|
52
|
-
const envPath = path.join(process.cwd(), '.env.local');
|
|
53
|
-
const envBackupPath = path.join(backupDir, 'env', '.env.local');
|
|
54
|
-
await ensureDir(path.dirname(envBackupPath));
|
|
55
|
-
await backupEnvFile(envPath, envBackupPath);
|
|
56
|
-
console.log(chalk.blue(`📁 Backup do .env.local: ${path.relative(process.cwd(), envBackupPath)}`));
|
|
57
|
-
|
|
58
|
-
const expectedKeys = [
|
|
59
|
-
'NEXT_PUBLIC_SUPABASE_URL',
|
|
60
|
-
'NEXT_PUBLIC_SUPABASE_ANON_KEY',
|
|
61
|
-
'SUPABASE_SERVICE_ROLE_KEY',
|
|
62
|
-
'SUPABASE_DB_URL',
|
|
63
|
-
'SUPABASE_PROJECT_ID',
|
|
64
|
-
'SUPABASE_ACCESS_TOKEN',
|
|
65
|
-
'SMOONB_OUTPUT_DIR'
|
|
66
|
-
];
|
|
67
|
-
const currentEnv = await readEnvFile(envPath);
|
|
68
|
-
const { finalEnv, dePara } = await mapEnvVariablesInteractively(currentEnv, expectedKeys);
|
|
69
|
-
await writeEnvFile(envPath, finalEnv);
|
|
70
|
-
await saveEnvMap(dePara, path.join(backupDir, 'env', 'env-map.json'));
|
|
71
|
-
console.log(chalk.green('✅ .env.local atualizado com sucesso. Nenhuma chave renomeada; valores sincronizados.'));
|
|
72
|
-
|
|
73
|
-
function getValue(expectedKey) {
|
|
74
|
-
const clientKey = Object.keys(dePara).find(k => dePara[k] === expectedKey);
|
|
75
|
-
return clientKey ? finalEnv[clientKey] : '';
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Recalcular outputDir a partir do ENV mapeado
|
|
79
|
-
const resolvedOutputDir = options.output || getValue('SMOONB_OUTPUT_DIR') || config.backup?.outputDir || './backups';
|
|
80
|
-
|
|
81
|
-
// Se mudou o outputDir, movemos o backupDir inicial para o novo local mantendo timestamp
|
|
82
|
-
const finalBackupDir = backupDir.startsWith(path.resolve(resolvedOutputDir))
|
|
83
|
-
? backupDir
|
|
84
|
-
: path.join(resolvedOutputDir, path.basename(backupDir));
|
|
85
|
-
if (finalBackupDir !== backupDir) {
|
|
86
|
-
await ensureDir(resolvedOutputDir);
|
|
87
|
-
await fs.rename(backupDir, finalBackupDir);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const projectId = getValue('SUPABASE_PROJECT_ID');
|
|
91
|
-
const accessToken = getValue('SUPABASE_ACCESS_TOKEN');
|
|
92
|
-
const databaseUrl = getValue('SUPABASE_DB_URL');
|
|
93
|
-
|
|
94
|
-
if (!databaseUrl) {
|
|
95
|
-
console.log(chalk.red('❌ DATABASE_URL NÃO CONFIGURADA'));
|
|
96
|
-
console.log('');
|
|
97
|
-
console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:'));
|
|
98
|
-
console.log(chalk.yellow(' 1. Configurar SUPABASE_DB_URL no .env.local'));
|
|
99
|
-
console.log(chalk.yellow(' 2. Repetir o comando de backup'));
|
|
100
|
-
console.log('');
|
|
101
|
-
console.log(chalk.blue('💡 Exemplo de configuração:'));
|
|
102
|
-
console.log(chalk.gray(' "databaseUrl": "postgresql://postgres:[senha]@db.[projeto].supabase.co:5432/postgres"'));
|
|
103
|
-
console.log('');
|
|
104
|
-
console.log(chalk.red('🚫 Backup cancelado - Configuração incompleta'));
|
|
105
|
-
process.exit(1);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (!accessToken) {
|
|
109
|
-
console.log(chalk.red('❌ ACCESS_TOKEN NÃO CONFIGURADO'));
|
|
110
|
-
console.log('');
|
|
111
|
-
console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:'));
|
|
112
|
-
console.log(chalk.yellow(' 1. Obter Personal Access Token do Supabase'));
|
|
113
|
-
console.log(chalk.yellow(' 2. Configurar SUPABASE_ACCESS_TOKEN no .env.local'));
|
|
114
|
-
console.log(chalk.yellow(' 3. Repetir o comando de backup'));
|
|
115
|
-
console.log('');
|
|
116
|
-
console.log(chalk.blue('🔗 Como obter o token:'));
|
|
117
|
-
console.log(chalk.gray(' 1. Acesse: https://supabase.com/dashboard/account/tokens'));
|
|
118
|
-
console.log(chalk.gray(' 2. Clique: "Generate new token"'));
|
|
119
|
-
console.log(chalk.gray(' 3. Copie o token (formato: sbp_...)'));
|
|
120
|
-
console.log('');
|
|
121
|
-
console.log(chalk.red('🚫 Backup cancelado - Token não configurado'));
|
|
122
|
-
process.exit(1);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
console.log(chalk.blue(`🚀 Iniciando backup do projeto: ${projectId}`));
|
|
126
|
-
console.log(chalk.gray(`🔍 Verificando dependências Docker...`));
|
|
127
|
-
|
|
128
|
-
// Verificar se é possível fazer backup completo via Docker
|
|
129
|
-
const backupCapability = await canPerformCompleteBackup();
|
|
130
|
-
|
|
131
|
-
if (backupCapability.canBackupComplete) {
|
|
132
|
-
console.log(chalk.green('✅ Docker Desktop detectado e funcionando'));
|
|
133
|
-
console.log(chalk.gray(`🐳 Versão: ${backupCapability.dockerStatus.version}`));
|
|
134
|
-
console.log('');
|
|
135
|
-
|
|
136
|
-
// Proceder com backup completo via Docker
|
|
137
|
-
// Flags de componentes (não afetam Database)
|
|
138
|
-
const flags = await askComponentsFlags();
|
|
139
|
-
return await performFullBackup({ projectId, accessToken, databaseUrl }, { ...options, flags, backupDir: finalBackupDir, outputDir: resolvedOutputDir });
|
|
140
|
-
} else {
|
|
141
|
-
// Mostrar mensagens educativas e encerrar elegantemente
|
|
142
|
-
showDockerMessagesAndExit(backupCapability.reason);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
} catch (error) {
|
|
146
|
-
console.error(chalk.red(`❌ Erro no backup: ${error.message}`));
|
|
147
|
-
process.exit(1);
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
// Função para backup completo via Docker
|
|
152
|
-
async function performFullBackup(envCfg, options) {
|
|
153
|
-
const { projectId, accessToken, databaseUrl } = envCfg;
|
|
154
|
-
const outputDir = options.outputDir;
|
|
155
|
-
const backupDir = options.backupDir;
|
|
156
|
-
|
|
157
|
-
console.log(chalk.blue(`📁 Diretório: ${backupDir}`));
|
|
158
|
-
console.log(chalk.gray(`🐳 Backup via Docker Desktop`));
|
|
159
|
-
|
|
160
|
-
const manifest = {
|
|
161
|
-
created_at: new Date().toISOString(),
|
|
162
|
-
project_id: projectId,
|
|
163
|
-
smoonb_version: require('../../package.json').version,
|
|
164
|
-
backup_type: 'pg_dumpall_docker_dashboard_compatible',
|
|
165
|
-
docker_version: await getDockerVersion(),
|
|
166
|
-
dashboard_compatible: true,
|
|
167
|
-
components: {}
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
// 1. Backup Database via pg_dumpall Docker (idêntico ao Dashboard)
|
|
171
|
-
console.log(chalk.blue('\n📊 1/8 - Backup da Database PostgreSQL via pg_dumpall Docker...'));
|
|
172
|
-
const databaseResult = await backupDatabase(databaseUrl, backupDir);
|
|
173
|
-
manifest.components.database = databaseResult;
|
|
174
|
-
|
|
175
|
-
// 1.5. Backup Database Separado (SQL files para troubleshooting)
|
|
176
|
-
console.log(chalk.blue('\n📊 1.5/8 - Backup da Database PostgreSQL (arquivos SQL separados)...'));
|
|
177
|
-
const dbSeparatedResult = await backupDatabaseSeparated(databaseUrl, backupDir, accessToken);
|
|
178
|
-
manifest.components.database_separated = {
|
|
179
|
-
success: dbSeparatedResult.success,
|
|
180
|
-
method: 'supabase-cli',
|
|
181
|
-
files: dbSeparatedResult.files || [],
|
|
182
|
-
total_size_kb: dbSeparatedResult.totalSizeKB || '0.0'
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
// 2. Backup Edge Functions via Docker
|
|
186
|
-
if (options.flags?.includeFunctions) {
|
|
187
|
-
console.log(chalk.blue('\n⚡ 2/8 - Backup das Edge Functions via Docker...'));
|
|
188
|
-
const functionsResult = await backupEdgeFunctionsWithDocker(projectId, accessToken, backupDir);
|
|
189
|
-
manifest.components.edge_functions = functionsResult;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// 3. Backup Auth Settings via API
|
|
193
|
-
if (options.flags?.includeAuth) {
|
|
194
|
-
console.log(chalk.blue('\n🔐 3/8 - Backup das Auth Settings via API...'));
|
|
195
|
-
const authResult = await backupAuthSettings(projectId, accessToken, backupDir);
|
|
196
|
-
manifest.components.auth_settings = authResult;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// 4. Backup Storage via API
|
|
200
|
-
if (options.flags?.includeStorage) {
|
|
201
|
-
console.log(chalk.blue('\n📦 4/8 - Backup do Storage via API...'));
|
|
202
|
-
const storageResult = await backupStorage(projectId, accessToken, backupDir);
|
|
203
|
-
manifest.components.storage = storageResult;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// 5. Backup Custom Roles via SQL
|
|
207
|
-
console.log(chalk.blue('\n👥 5/8 - Backup dos Custom Roles via SQL...'));
|
|
208
|
-
const rolesResult = await backupCustomRoles(databaseUrl, backupDir, accessToken);
|
|
209
|
-
manifest.components.custom_roles = rolesResult;
|
|
210
|
-
|
|
211
|
-
// 6. Backup das Database Extensions and Settings via SQL
|
|
212
|
-
console.log(chalk.blue('\n🔧 6/8 - Backup das Database Extensions and Settings via SQL...'));
|
|
213
|
-
const databaseSettingsResult = await backupDatabaseSettings(databaseUrl, projectId, backupDir);
|
|
214
|
-
manifest.components.database_settings = databaseSettingsResult;
|
|
215
|
-
|
|
216
|
-
// 7. Backup Realtime Settings via Captura Interativa
|
|
217
|
-
if (options.flags?.includeRealtime) {
|
|
218
|
-
console.log(chalk.blue('\n🔄 7/8 - Backup das Realtime Settings via Captura Interativa...'));
|
|
219
|
-
const realtimeResult = await backupRealtimeSettings(projectId, backupDir, options.skipRealtime);
|
|
220
|
-
manifest.components.realtime = realtimeResult;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Salvar manifest
|
|
224
|
-
await writeJson(path.join(backupDir, 'backup-manifest.json'), manifest);
|
|
225
|
-
|
|
226
|
-
console.log(chalk.green('\n🎉 BACKUP COMPLETO FINALIZADO VIA DOCKER!'));
|
|
227
|
-
console.log(chalk.blue(`📁 Localização: ${backupDir}`));
|
|
228
|
-
console.log(chalk.green(`📊 Database: ${databaseResult.fileName} (${databaseResult.size} KB) - Idêntico ao Dashboard`));
|
|
229
|
-
console.log(chalk.green(`📊 Database SQL: ${dbSeparatedResult.files?.length || 0} arquivos separados (${dbSeparatedResult.totalSizeKB} KB) - Para troubleshooting`));
|
|
230
|
-
console.log(chalk.green(`🔧 Database Settings: ${databaseSettingsResult.fileName} (${databaseSettingsResult.size} KB) - Extensions e Configurações`));
|
|
231
|
-
if (options.flags?.includeFunctions && manifest.components.edge_functions) {
|
|
232
|
-
const functionsResult = manifest.components.edge_functions;
|
|
233
|
-
console.log(chalk.green(`⚡ Edge Functions: ${functionsResult.success_count || 0}/${functionsResult.functions_count || 0} functions baixadas via Docker`));
|
|
234
|
-
}
|
|
235
|
-
if (options.flags?.includeAuth && manifest.components.auth_settings) {
|
|
236
|
-
const authResult = manifest.components.auth_settings;
|
|
237
|
-
console.log(chalk.green(`🔐 Auth Settings: ${authResult.success ? 'Exportadas via API' : 'Falharam'}`));
|
|
238
|
-
}
|
|
239
|
-
if (options.flags?.includeStorage && manifest.components.storage) {
|
|
240
|
-
const storageResult = manifest.components.storage;
|
|
241
|
-
console.log(chalk.green(`📦 Storage: ${storageResult.buckets?.length || 0} buckets verificados via API`));
|
|
242
|
-
}
|
|
243
|
-
console.log(chalk.green(`👥 Custom Roles: ${rolesResult.roles?.length || 0} roles exportados via SQL`));
|
|
244
|
-
// Determinar mensagem correta baseada no método usado
|
|
245
|
-
if (options.flags?.includeRealtime && manifest.components.realtime) {
|
|
246
|
-
const realtimeResult = manifest.components.realtime;
|
|
247
|
-
let realtimeMessage = 'Falharam';
|
|
248
|
-
if (realtimeResult.success) {
|
|
249
|
-
if (options.skipRealtime) {
|
|
250
|
-
realtimeMessage = 'Configurações copiadas do backup anterior';
|
|
251
|
-
} else {
|
|
252
|
-
realtimeMessage = 'Configurações capturadas interativamente';
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
console.log(chalk.green(`🔄 Realtime: ${realtimeMessage}`));
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// report.json
|
|
259
|
-
await writeJson(path.join(backupDir, 'report.json'), {
|
|
260
|
-
process: 'backup',
|
|
261
|
-
created_at: manifest.created_at,
|
|
262
|
-
project_id: manifest.project_id,
|
|
263
|
-
assets: {
|
|
264
|
-
env: path.join(backupDir, 'env', '.env.local'),
|
|
265
|
-
env_map: path.join(backupDir, 'env', 'env-map.json'),
|
|
266
|
-
manifest: path.join(backupDir, 'backup-manifest.json')
|
|
267
|
-
},
|
|
268
|
-
components: {
|
|
269
|
-
includeFunctions: !!options.flags?.includeFunctions,
|
|
270
|
-
includeStorage: !!options.flags?.includeStorage,
|
|
271
|
-
includeAuth: !!options.flags?.includeAuth,
|
|
272
|
-
includeRealtime: !!options.flags?.includeRealtime
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
return { success: true, backupDir, manifest };
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Função para mostrar mensagens educativas e encerrar elegantemente
|
|
280
|
-
function showDockerMessagesAndExit(reason) {
|
|
281
|
-
console.log('');
|
|
282
|
-
|
|
283
|
-
switch (reason) {
|
|
284
|
-
case 'docker_not_installed':
|
|
285
|
-
console.log(chalk.red('❌ DOCKER DESKTOP NÃO ENCONTRADO'));
|
|
286
|
-
console.log('');
|
|
287
|
-
console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:'));
|
|
288
|
-
console.log(chalk.yellow(' 1. Instalar Docker Desktop'));
|
|
289
|
-
console.log(chalk.yellow(' 2. Executar Docker Desktop'));
|
|
290
|
-
console.log(chalk.yellow(' 3. Repetir o comando de backup'));
|
|
291
|
-
console.log('');
|
|
292
|
-
console.log(chalk.blue('🔗 Download: https://docs.docker.com/desktop/install/'));
|
|
293
|
-
console.log('');
|
|
294
|
-
console.log(chalk.gray('💡 O Docker Desktop é obrigatório para backup completo do Supabase'));
|
|
295
|
-
console.log(chalk.gray(' - Database PostgreSQL'));
|
|
296
|
-
console.log(chalk.gray(' - Edge Functions'));
|
|
297
|
-
console.log(chalk.gray(' - Todos os componentes via Supabase CLI'));
|
|
298
|
-
break;
|
|
299
|
-
|
|
300
|
-
case 'docker_not_running':
|
|
301
|
-
console.log(chalk.red('❌ DOCKER DESKTOP NÃO ESTÁ EXECUTANDO'));
|
|
302
|
-
console.log('');
|
|
303
|
-
console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:'));
|
|
304
|
-
console.log(chalk.yellow(' 1. Abrir Docker Desktop'));
|
|
305
|
-
console.log(chalk.yellow(' 2. Aguardar inicialização completa'));
|
|
306
|
-
console.log(chalk.yellow(' 3. Repetir o comando de backup'));
|
|
307
|
-
console.log('');
|
|
308
|
-
console.log(chalk.blue('💡 Dica: Docker Desktop deve estar rodando em segundo plano'));
|
|
309
|
-
console.log('');
|
|
310
|
-
console.log(chalk.gray('💡 O Docker Desktop é obrigatório para backup completo do Supabase'));
|
|
311
|
-
console.log(chalk.gray(' - Database PostgreSQL'));
|
|
312
|
-
console.log(chalk.gray(' - Edge Functions'));
|
|
313
|
-
console.log(chalk.gray(' - Todos os componentes via Supabase CLI'));
|
|
314
|
-
break;
|
|
315
|
-
|
|
316
|
-
case 'supabase_cli_not_found':
|
|
317
|
-
console.log(chalk.red('❌ SUPABASE CLI NÃO ENCONTRADO'));
|
|
318
|
-
console.log('');
|
|
319
|
-
console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:'));
|
|
320
|
-
console.log(chalk.yellow(' 1. Instalar Supabase CLI'));
|
|
321
|
-
console.log(chalk.yellow(' 2. Repetir o comando de backup'));
|
|
322
|
-
console.log('');
|
|
323
|
-
console.log(chalk.blue('🔗 Instalação: npm install -g supabase'));
|
|
324
|
-
console.log('');
|
|
325
|
-
console.log(chalk.gray('💡 O Supabase CLI é obrigatório para backup completo do Supabase'));
|
|
326
|
-
console.log(chalk.gray(' - Database PostgreSQL'));
|
|
327
|
-
console.log(chalk.gray(' - Edge Functions'));
|
|
328
|
-
console.log(chalk.gray(' - Todos os componentes via Docker'));
|
|
329
|
-
break;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
console.log('');
|
|
333
|
-
console.log(chalk.red('🚫 Backup cancelado - Pré-requisitos não atendidos'));
|
|
334
|
-
console.log(chalk.gray(' Instale os componentes necessários e tente novamente'));
|
|
335
|
-
console.log('');
|
|
336
|
-
|
|
337
|
-
process.exit(1);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Backup da database usando pg_dumpall via Docker (idêntico ao Supabase Dashboard)
|
|
341
|
-
async function backupDatabase(databaseUrl, backupDir) {
|
|
342
|
-
try {
|
|
343
|
-
console.log(chalk.gray(' - Criando backup completo via pg_dumpall...'));
|
|
344
|
-
|
|
345
|
-
const { execSync } = require('child_process');
|
|
346
|
-
|
|
347
|
-
// Extrair credenciais da databaseUrl
|
|
348
|
-
const urlMatch = databaseUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
|
|
349
|
-
|
|
350
|
-
if (!urlMatch) {
|
|
351
|
-
throw new Error('Database URL inválida');
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const [, username, password, host, port, database] = urlMatch;
|
|
355
|
-
|
|
356
|
-
// Gerar nome do arquivo igual ao dashboard
|
|
357
|
-
const now = new Date();
|
|
358
|
-
const day = String(now.getDate()).padStart(2, '0');
|
|
359
|
-
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
360
|
-
const year = now.getFullYear();
|
|
361
|
-
const hours = String(now.getHours()).padStart(2, '0');
|
|
362
|
-
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
363
|
-
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
364
|
-
|
|
365
|
-
const fileName = `db_cluster-${day}-${month}-${year}@${hours}-${minutes}-${seconds}.backup`;
|
|
366
|
-
|
|
367
|
-
// CORREÇÃO: Usar caminho absoluto igual às Edge Functions
|
|
368
|
-
const backupDirAbs = path.resolve(backupDir);
|
|
369
|
-
|
|
370
|
-
// Comando pg_dumpall via Docker (mesma abordagem das Edge Functions)
|
|
371
|
-
const dockerCmd = [
|
|
372
|
-
'docker run --rm --network host',
|
|
373
|
-
`-v "${backupDirAbs}:/host"`,
|
|
374
|
-
`-e PGPASSWORD="${password}"`,
|
|
375
|
-
'postgres:17 pg_dumpall',
|
|
376
|
-
`-h ${host}`,
|
|
377
|
-
`-p ${port}`,
|
|
378
|
-
`-U ${username}`,
|
|
379
|
-
`-f /host/${fileName}`
|
|
380
|
-
].join(' ');
|
|
381
|
-
|
|
382
|
-
console.log(chalk.gray(` - Executando pg_dumpall via Docker...`));
|
|
383
|
-
execSync(dockerCmd, { stdio: 'pipe' });
|
|
384
|
-
|
|
385
|
-
// Compactar igual ao Supabase Dashboard
|
|
386
|
-
const gzipCmd = [
|
|
387
|
-
'docker run --rm',
|
|
388
|
-
`-v "${backupDirAbs}:/host"`,
|
|
389
|
-
`postgres:17 gzip /host/${fileName}`
|
|
390
|
-
].join(' ');
|
|
391
|
-
|
|
392
|
-
execSync(gzipCmd, { stdio: 'pipe' });
|
|
393
|
-
|
|
394
|
-
const finalFileName = `${fileName}.gz`;
|
|
395
|
-
const stats = await fs.stat(path.join(backupDir, finalFileName));
|
|
396
|
-
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
397
|
-
|
|
398
|
-
console.log(chalk.green(` ✅ Database backup: ${finalFileName} (${sizeKB} KB)`));
|
|
399
|
-
|
|
400
|
-
return { success: true, size: sizeKB, fileName: finalFileName };
|
|
401
|
-
} catch (error) {
|
|
402
|
-
console.log(chalk.yellow(` ⚠️ Erro no backup do database: ${error.message}`));
|
|
403
|
-
return { success: false };
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Backup da database usando arquivos SQL separados via Supabase CLI (para troubleshooting)
|
|
408
|
-
async function backupDatabaseSeparated(databaseUrl, backupDir, accessToken) {
|
|
409
|
-
try {
|
|
410
|
-
console.log(chalk.gray(' - Criando backups SQL separados via Supabase CLI...'));
|
|
411
|
-
|
|
412
|
-
const { execSync } = require('child_process');
|
|
413
|
-
const dbUrl = databaseUrl;
|
|
414
|
-
const files = [];
|
|
415
|
-
let totalSizeKB = 0;
|
|
416
|
-
|
|
417
|
-
// 1. Backup do Schema
|
|
418
|
-
console.log(chalk.gray(' - Exportando schema...'));
|
|
419
|
-
const schemaFile = path.join(backupDir, 'schema.sql');
|
|
420
|
-
|
|
421
|
-
try {
|
|
422
|
-
execSync(`supabase db dump --db-url "${dbUrl}" -f "${schemaFile}"`, { stdio: 'pipe', env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken || '' } });
|
|
423
|
-
const stats = await fs.stat(schemaFile);
|
|
424
|
-
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
425
|
-
files.push({ filename: 'schema.sql', sizeKB });
|
|
426
|
-
totalSizeKB += parseFloat(sizeKB);
|
|
427
|
-
console.log(chalk.green(` ✅ Schema: ${sizeKB} KB`));
|
|
428
|
-
} catch (error) {
|
|
429
|
-
console.log(chalk.yellow(` ⚠️ Erro no schema: ${error.message}`));
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// 2. Backup dos Dados
|
|
433
|
-
console.log(chalk.gray(' - Exportando dados...'));
|
|
434
|
-
const dataFile = path.join(backupDir, 'data.sql');
|
|
435
|
-
|
|
436
|
-
try {
|
|
437
|
-
execSync(`supabase db dump --db-url "${dbUrl}" --data-only -f "${dataFile}"`, { stdio: 'pipe', env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken || '' } });
|
|
438
|
-
const stats = await fs.stat(dataFile);
|
|
439
|
-
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
440
|
-
files.push({ filename: 'data.sql', sizeKB });
|
|
441
|
-
totalSizeKB += parseFloat(sizeKB);
|
|
442
|
-
console.log(chalk.green(` ✅ Data: ${sizeKB} KB`));
|
|
443
|
-
} catch (error) {
|
|
444
|
-
console.log(chalk.yellow(` ⚠️ Erro nos dados: ${error.message}`));
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// 3. Backup dos Roles
|
|
448
|
-
console.log(chalk.gray(' - Exportando roles...'));
|
|
449
|
-
const rolesFile = path.join(backupDir, 'roles.sql');
|
|
450
|
-
|
|
451
|
-
try {
|
|
452
|
-
execSync(`supabase db dump --db-url "${dbUrl}" --role-only -f "${rolesFile}"`, { stdio: 'pipe', env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken || '' } });
|
|
453
|
-
const stats = await fs.stat(rolesFile);
|
|
454
|
-
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
455
|
-
files.push({ filename: 'roles.sql', sizeKB });
|
|
456
|
-
totalSizeKB += parseFloat(sizeKB);
|
|
457
|
-
console.log(chalk.green(` ✅ Roles: ${sizeKB} KB`));
|
|
458
|
-
} catch (error) {
|
|
459
|
-
console.log(chalk.yellow(` ⚠️ Erro nos roles: ${error.message}`));
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
return {
|
|
463
|
-
success: files.length > 0,
|
|
464
|
-
files,
|
|
465
|
-
totalSizeKB: totalSizeKB.toFixed(1)
|
|
466
|
-
};
|
|
467
|
-
|
|
468
|
-
} catch (error) {
|
|
469
|
-
console.log(chalk.yellow(` ⚠️ Erro nos backups SQL separados: ${error.message}`));
|
|
470
|
-
return { success: false, files: [], totalSizeKB: '0.0' };
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Backup das Edge Functions via Docker
|
|
475
|
-
async function backupEdgeFunctionsWithDocker(projectId, accessToken, backupDir) {
|
|
476
|
-
try {
|
|
477
|
-
const functionsDir = path.join(backupDir, 'edge-functions');
|
|
478
|
-
await ensureDir(functionsDir);
|
|
479
|
-
|
|
480
|
-
console.log(chalk.gray(' - Listando Edge Functions via Management API...'));
|
|
481
|
-
|
|
482
|
-
// ✅ Usar fetch direto para Management API com Personal Access Token
|
|
483
|
-
const functionsResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/functions`, {
|
|
484
|
-
headers: {
|
|
485
|
-
'Authorization': `Bearer ${accessToken}`,
|
|
486
|
-
'Content-Type': 'application/json'
|
|
487
|
-
}
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
if (!functionsResponse.ok) {
|
|
491
|
-
console.log(chalk.yellow(` ⚠️ Erro ao listar Edge Functions: ${functionsResponse.status} ${functionsResponse.statusText}`));
|
|
492
|
-
return { success: false, reason: 'api_error', functions: [] };
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const functions = await functionsResponse.json();
|
|
496
|
-
|
|
497
|
-
if (!functions || functions.length === 0) {
|
|
498
|
-
console.log(chalk.gray(' - Nenhuma Edge Function encontrada'));
|
|
499
|
-
await writeJson(path.join(functionsDir, 'README.md'), {
|
|
500
|
-
message: 'Nenhuma Edge Function encontrada neste projeto'
|
|
501
|
-
});
|
|
502
|
-
return { success: true, reason: 'no_functions', functions: [] };
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
console.log(chalk.gray(` - Encontradas ${functions.length} Edge Function(s)`));
|
|
506
|
-
|
|
507
|
-
const downloadedFunctions = [];
|
|
508
|
-
let successCount = 0;
|
|
509
|
-
let errorCount = 0;
|
|
510
|
-
|
|
511
|
-
// ✅ Baixar cada Edge Function via Supabase CLI
|
|
512
|
-
// Nota: O CLI ignora o cwd e sempre baixa para supabase/functions
|
|
513
|
-
for (const func of functions) {
|
|
514
|
-
try {
|
|
515
|
-
console.log(chalk.gray(` - Baixando: ${func.name}...`));
|
|
516
|
-
|
|
517
|
-
// Criar diretório da função NO BACKUP
|
|
518
|
-
const functionTargetDir = path.join(functionsDir, func.name);
|
|
519
|
-
await ensureDir(functionTargetDir);
|
|
520
|
-
|
|
521
|
-
// Diretório temporário onde o supabase CLI irá baixar (supabase/functions)
|
|
522
|
-
const tempDownloadDir = path.join(process.cwd(), 'supabase', 'functions', func.name);
|
|
523
|
-
|
|
524
|
-
// Baixar Edge Function via Supabase CLI (sempre vai para supabase/functions)
|
|
525
|
-
const { execSync } = require('child_process');
|
|
526
|
-
|
|
527
|
-
execSync(`supabase functions download ${func.name}`, {
|
|
528
|
-
timeout: 60000,
|
|
529
|
-
stdio: 'pipe'
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
// ✅ COPIAR arquivos de supabase/functions para o backup
|
|
533
|
-
try {
|
|
534
|
-
const stat = await fs.stat(tempDownloadDir);
|
|
535
|
-
if (stat.isDirectory()) {
|
|
536
|
-
const files = await fs.readdir(tempDownloadDir);
|
|
537
|
-
for (const file of files) {
|
|
538
|
-
const srcPath = path.join(tempDownloadDir, file);
|
|
539
|
-
const dstPath = path.join(functionTargetDir, file);
|
|
540
|
-
|
|
541
|
-
const fileStats = await fs.stat(srcPath);
|
|
542
|
-
if (fileStats.isDirectory()) {
|
|
543
|
-
// Copiar diretórios recursivamente
|
|
544
|
-
await fs.cp(srcPath, dstPath, { recursive: true });
|
|
545
|
-
} else {
|
|
546
|
-
// Copiar arquivos
|
|
547
|
-
await fs.copyFile(srcPath, dstPath);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
} catch (copyError) {
|
|
552
|
-
// Arquivos não foram baixados, continuar
|
|
553
|
-
console.log(chalk.yellow(` ⚠️ Nenhum arquivo encontrado em ${tempDownloadDir}`));
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// ✅ LIMPAR supabase/functions após copiar
|
|
557
|
-
try {
|
|
558
|
-
await fs.rm(tempDownloadDir, { recursive: true, force: true });
|
|
559
|
-
} catch (cleanError) {
|
|
560
|
-
// Ignorar erro de limpeza
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
console.log(chalk.green(` ✅ ${func.name} baixada com sucesso`));
|
|
564
|
-
successCount++;
|
|
565
|
-
|
|
566
|
-
downloadedFunctions.push({
|
|
567
|
-
name: func.name,
|
|
568
|
-
slug: func.name,
|
|
569
|
-
version: func.version || 'unknown',
|
|
570
|
-
files: await fs.readdir(functionTargetDir).catch(() => [])
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
} catch (error) {
|
|
574
|
-
console.log(chalk.yellow(` ⚠️ Erro ao baixar ${func.name}: ${error.message}`));
|
|
575
|
-
errorCount++;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
console.log(chalk.green(`📊 Backup de Edge Functions concluído:`));
|
|
580
|
-
console.log(chalk.green(` ✅ Sucessos: ${successCount}`));
|
|
581
|
-
console.log(chalk.green(` ❌ Erros: ${errorCount}`));
|
|
582
|
-
|
|
583
|
-
return {
|
|
584
|
-
success: true,
|
|
585
|
-
reason: 'success',
|
|
586
|
-
functions: downloadedFunctions,
|
|
587
|
-
functions_count: functions.length,
|
|
588
|
-
success_count: successCount,
|
|
589
|
-
error_count: errorCount,
|
|
590
|
-
method: 'docker'
|
|
591
|
-
};
|
|
592
|
-
|
|
593
|
-
} catch (error) {
|
|
594
|
-
console.log(chalk.yellow(` ⚠️ Erro durante backup de Edge Functions: ${error.message}`));
|
|
595
|
-
console.log('⏭️ Continuando com outros componentes...');
|
|
596
|
-
return { success: false, reason: 'download_error', error: error.message, functions: [] };
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// Backup das Auth Settings via Management API
|
|
601
|
-
async function backupAuthSettings(projectId, accessToken, backupDir) {
|
|
602
|
-
try {
|
|
603
|
-
console.log(chalk.gray(' - Exportando configurações de Auth via Management API...'));
|
|
604
|
-
|
|
605
|
-
// ✅ Usar fetch direto para Management API com Personal Access Token
|
|
606
|
-
const authResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/config/auth`, {
|
|
607
|
-
headers: {
|
|
608
|
-
'Authorization': `Bearer ${accessToken}`,
|
|
609
|
-
'Content-Type': 'application/json'
|
|
610
|
-
}
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
if (!authResponse.ok) {
|
|
614
|
-
console.log(chalk.yellow(` ⚠️ Erro ao obter Auth Settings: ${authResponse.status} ${authResponse.statusText}`));
|
|
615
|
-
return { success: false };
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
const authSettings = await authResponse.json();
|
|
619
|
-
|
|
620
|
-
// Salvar configurações de Auth
|
|
621
|
-
const authSettingsPath = path.join(backupDir, 'auth-settings.json');
|
|
622
|
-
await writeJson(authSettingsPath, {
|
|
623
|
-
project_id: projectId,
|
|
624
|
-
timestamp: new Date().toISOString(),
|
|
625
|
-
settings: authSettings
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
console.log(chalk.green(`✅ Auth Settings exportadas: ${path.basename(authSettingsPath)}`));
|
|
629
|
-
return { success: true };
|
|
630
|
-
|
|
631
|
-
} catch (error) {
|
|
632
|
-
console.log(chalk.yellow(` ⚠️ Erro no backup das Auth Settings: ${error.message}`));
|
|
633
|
-
return { success: false };
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// Backup do Storage via Supabase API
|
|
638
|
-
async function backupStorage(projectId, accessToken, backupDir) {
|
|
639
|
-
try {
|
|
640
|
-
const storageDir = path.join(backupDir, 'storage');
|
|
641
|
-
await ensureDir(storageDir);
|
|
642
|
-
|
|
643
|
-
console.log(chalk.gray(' - Listando buckets de Storage via Management API...'));
|
|
644
|
-
|
|
645
|
-
// ✅ Usar fetch direto para Management API com Personal Access Token
|
|
646
|
-
const storageResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/storage/buckets`, {
|
|
647
|
-
headers: {
|
|
648
|
-
'Authorization': `Bearer ${accessToken}`,
|
|
649
|
-
'Content-Type': 'application/json'
|
|
650
|
-
}
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
if (!storageResponse.ok) {
|
|
654
|
-
console.log(chalk.yellow(` ⚠️ Erro ao listar buckets: ${storageResponse.status} ${storageResponse.statusText}`));
|
|
655
|
-
return { success: false, buckets: [] };
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
const buckets = await storageResponse.json();
|
|
659
|
-
|
|
660
|
-
if (!buckets || buckets.length === 0) {
|
|
661
|
-
console.log(chalk.gray(' - Nenhum bucket encontrado'));
|
|
662
|
-
await writeJson(path.join(storageDir, 'README.md'), {
|
|
663
|
-
message: 'Nenhum bucket de Storage encontrado neste projeto'
|
|
664
|
-
});
|
|
665
|
-
return { success: true, buckets: [] };
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
console.log(chalk.gray(` - Encontrados ${buckets.length} buckets`));
|
|
669
|
-
|
|
670
|
-
const processedBuckets = [];
|
|
671
|
-
|
|
672
|
-
for (const bucket of buckets || []) {
|
|
673
|
-
try {
|
|
674
|
-
console.log(chalk.gray(` - Processando bucket: ${bucket.name}`));
|
|
675
|
-
|
|
676
|
-
// ✅ Listar objetos do bucket via Management API com Personal Access Token
|
|
677
|
-
const objectsResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/storage/buckets/${bucket.name}/objects`, {
|
|
678
|
-
headers: {
|
|
679
|
-
'Authorization': `Bearer ${accessToken}`,
|
|
680
|
-
'Content-Type': 'application/json'
|
|
681
|
-
}
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
let objects = [];
|
|
685
|
-
if (objectsResponse.ok) {
|
|
686
|
-
objects = await objectsResponse.json();
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
const bucketInfo = {
|
|
690
|
-
id: bucket.id,
|
|
691
|
-
name: bucket.name,
|
|
692
|
-
public: bucket.public,
|
|
693
|
-
file_size_limit: bucket.file_size_limit,
|
|
694
|
-
allowed_mime_types: bucket.allowed_mime_types,
|
|
695
|
-
objects: objects || []
|
|
696
|
-
};
|
|
697
|
-
|
|
698
|
-
// Salvar informações do bucket
|
|
699
|
-
const bucketPath = path.join(storageDir, `${bucket.name}.json`);
|
|
700
|
-
await writeJson(bucketPath, bucketInfo);
|
|
701
|
-
|
|
702
|
-
processedBuckets.push({
|
|
703
|
-
name: bucket.name,
|
|
704
|
-
objectCount: objects?.length || 0
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
console.log(chalk.green(` ✅ Bucket ${bucket.name}: ${objects?.length || 0} objetos`));
|
|
708
|
-
} catch (error) {
|
|
709
|
-
console.log(chalk.yellow(` ⚠️ Erro ao processar bucket ${bucket.name}: ${error.message}`));
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
console.log(chalk.green(`✅ Storage backupado: ${processedBuckets.length} buckets`));
|
|
714
|
-
return { success: true, buckets: processedBuckets };
|
|
715
|
-
} catch (error) {
|
|
716
|
-
console.log(chalk.yellow(`⚠️ Erro no backup do Storage: ${error.message}`));
|
|
717
|
-
return { success: false, buckets: [] };
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// Backup dos Custom Roles via Docker
|
|
722
|
-
async function backupCustomRoles(databaseUrl, backupDir, accessToken) {
|
|
723
|
-
try {
|
|
724
|
-
console.log(chalk.gray(' - Exportando Custom Roles via Docker...'));
|
|
725
|
-
|
|
726
|
-
const customRolesFile = path.join(backupDir, 'custom-roles.sql');
|
|
727
|
-
|
|
728
|
-
try {
|
|
729
|
-
// ✅ Usar Supabase CLI via Docker para roles
|
|
730
|
-
await execAsync(`supabase db dump --db-url "${databaseUrl}" --role-only -f "${customRolesFile}"`, { env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken || '' } });
|
|
731
|
-
|
|
732
|
-
const stats = await fs.stat(customRolesFile);
|
|
733
|
-
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
734
|
-
|
|
735
|
-
console.log(chalk.green(` ✅ Custom Roles exportados via Docker: ${sizeKB} KB`));
|
|
736
|
-
|
|
737
|
-
return { success: true, roles: [{ filename: 'custom-roles.sql', sizeKB }] };
|
|
738
|
-
} catch (error) {
|
|
739
|
-
console.log(chalk.yellow(` ⚠️ Erro ao exportar Custom Roles via Docker: ${error.message}`));
|
|
740
|
-
return { success: false, roles: [] };
|
|
741
|
-
}
|
|
742
|
-
} catch (error) {
|
|
743
|
-
console.log(chalk.yellow(` ⚠️ Erro no backup dos Custom Roles: ${error.message}`));
|
|
744
|
-
return { success: false, roles: [] };
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
// Backup das Database Extensions and Settings via SQL
|
|
749
|
-
async function backupDatabaseSettings(databaseUrl, projectId, backupDir) {
|
|
750
|
-
try {
|
|
751
|
-
console.log(chalk.gray(' - Capturando Database Extensions and Settings...'));
|
|
752
|
-
|
|
753
|
-
const { execSync } = require('child_process');
|
|
754
|
-
|
|
755
|
-
// Extrair credenciais da databaseUrl
|
|
756
|
-
const urlMatch = databaseUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
|
|
757
|
-
|
|
758
|
-
if (!urlMatch) {
|
|
759
|
-
throw new Error('Database URL inválida');
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
const [, username, password, host, port, database] = urlMatch;
|
|
763
|
-
|
|
764
|
-
// Gerar nome do arquivo
|
|
765
|
-
const now = new Date();
|
|
766
|
-
const day = String(now.getDate()).padStart(2, '0');
|
|
767
|
-
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
768
|
-
const year = now.getFullYear();
|
|
769
|
-
const hours = String(now.getHours()).padStart(2, '0');
|
|
770
|
-
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
771
|
-
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
772
|
-
|
|
773
|
-
const fileName = `database-settings-${day}-${month}-${year}@${hours}-${minutes}-${seconds}.json`;
|
|
774
|
-
|
|
775
|
-
// Usar caminho absoluto igual às outras funções
|
|
776
|
-
const backupDirAbs = path.resolve(backupDir);
|
|
777
|
-
|
|
778
|
-
// Script SQL para capturar todas as configurações
|
|
779
|
-
const sqlScript = `
|
|
780
|
-
-- Database Extensions and Settings Backup
|
|
781
|
-
-- Generated at: ${new Date().toISOString()}
|
|
782
|
-
|
|
783
|
-
-- 1. Capturar extensões instaladas
|
|
784
|
-
SELECT json_agg(
|
|
785
|
-
json_build_object(
|
|
786
|
-
'name', extname,
|
|
787
|
-
'version', extversion,
|
|
788
|
-
'schema', extnamespace::regnamespace
|
|
789
|
-
)
|
|
790
|
-
) as extensions
|
|
791
|
-
FROM pg_extension;
|
|
792
|
-
|
|
793
|
-
-- 2. Capturar configurações PostgreSQL importantes
|
|
794
|
-
SELECT json_agg(
|
|
795
|
-
json_build_object(
|
|
796
|
-
'name', name,
|
|
797
|
-
'setting', setting,
|
|
798
|
-
'unit', unit,
|
|
799
|
-
'context', context,
|
|
800
|
-
'description', short_desc
|
|
801
|
-
)
|
|
802
|
-
) as postgres_settings
|
|
803
|
-
FROM pg_settings
|
|
804
|
-
WHERE name IN (
|
|
805
|
-
'statement_timeout',
|
|
806
|
-
'idle_in_transaction_session_timeout',
|
|
807
|
-
'lock_timeout',
|
|
808
|
-
'shared_buffers',
|
|
809
|
-
'work_mem',
|
|
810
|
-
'maintenance_work_mem',
|
|
811
|
-
'effective_cache_size',
|
|
812
|
-
'max_connections',
|
|
813
|
-
'log_statement',
|
|
814
|
-
'log_min_duration_statement',
|
|
815
|
-
'timezone',
|
|
816
|
-
'log_timezone',
|
|
817
|
-
'default_transaction_isolation',
|
|
818
|
-
'default_transaction_read_only',
|
|
819
|
-
'checkpoint_completion_target',
|
|
820
|
-
'wal_buffers',
|
|
821
|
-
'max_wal_size',
|
|
822
|
-
'min_wal_size'
|
|
823
|
-
);
|
|
824
|
-
|
|
825
|
-
-- 3. Capturar configurações específicas dos roles Supabase
|
|
826
|
-
SELECT json_agg(
|
|
827
|
-
json_build_object(
|
|
828
|
-
'role', rolname,
|
|
829
|
-
'config', rolconfig
|
|
830
|
-
)
|
|
831
|
-
) as role_configurations
|
|
832
|
-
FROM pg_roles
|
|
833
|
-
WHERE rolname IN ('anon', 'authenticated', 'authenticator', 'postgres', 'service_role')
|
|
834
|
-
AND rolconfig IS NOT NULL;
|
|
835
|
-
|
|
836
|
-
-- 4. Capturar configurações de PGAudit (se existir)
|
|
837
|
-
SELECT json_agg(
|
|
838
|
-
json_build_object(
|
|
839
|
-
'role', rolname,
|
|
840
|
-
'config', rolconfig
|
|
841
|
-
)
|
|
842
|
-
) as pgaudit_configurations
|
|
843
|
-
FROM pg_roles
|
|
844
|
-
WHERE rolconfig IS NOT NULL
|
|
845
|
-
AND EXISTS (
|
|
846
|
-
SELECT 1 FROM unnest(rolconfig) AS config
|
|
847
|
-
WHERE config LIKE '%pgaudit%'
|
|
848
|
-
);
|
|
849
|
-
`;
|
|
850
|
-
|
|
851
|
-
// Salvar script SQL temporário
|
|
852
|
-
const sqlFile = path.join(backupDir, 'temp_settings.sql');
|
|
853
|
-
await fs.writeFile(sqlFile, sqlScript);
|
|
854
|
-
|
|
855
|
-
// Executar via Docker
|
|
856
|
-
const dockerCmd = [
|
|
857
|
-
'docker run --rm --network host',
|
|
858
|
-
`-v "${backupDirAbs}:/host"`,
|
|
859
|
-
`-e PGPASSWORD="${password}"`,
|
|
860
|
-
'postgres:17 psql',
|
|
861
|
-
`-h ${host}`,
|
|
862
|
-
`-p ${port}`,
|
|
863
|
-
`-U ${username}`,
|
|
864
|
-
`-d ${database}`,
|
|
865
|
-
'-f /host/temp_settings.sql',
|
|
866
|
-
'-t', // Tuples only
|
|
867
|
-
'-A' // Unaligned output
|
|
868
|
-
].join(' ');
|
|
869
|
-
|
|
870
|
-
console.log(chalk.gray(' - Executando queries de configurações via Docker...'));
|
|
871
|
-
const output = execSync(dockerCmd, { stdio: 'pipe', encoding: 'utf8' });
|
|
872
|
-
|
|
873
|
-
// Processar output e criar JSON estruturado
|
|
874
|
-
const lines = output.trim().split('\n').filter(line => line.trim());
|
|
875
|
-
|
|
876
|
-
const result = {
|
|
877
|
-
database_settings: {
|
|
878
|
-
note: "Configurações específicas do database Supabase capturadas via SQL",
|
|
879
|
-
captured_at: new Date().toISOString(),
|
|
880
|
-
project_id: projectId,
|
|
881
|
-
extensions: lines[0] ? JSON.parse(lines[0]) : [],
|
|
882
|
-
postgres_settings: lines[1] ? JSON.parse(lines[1]) : [],
|
|
883
|
-
role_configurations: lines[2] ? JSON.parse(lines[2]) : [],
|
|
884
|
-
pgaudit_configurations: lines[3] ? JSON.parse(lines[3]) : [],
|
|
885
|
-
restore_instructions: {
|
|
886
|
-
note: "Estas configurações precisam ser aplicadas manualmente após a restauração do database",
|
|
887
|
-
steps: [
|
|
888
|
-
"1. Restaurar o database usando o arquivo .backup.gz",
|
|
889
|
-
"2. Aplicar configurações de Postgres via SQL:",
|
|
890
|
-
" ALTER DATABASE postgres SET setting_name TO 'value';",
|
|
891
|
-
"3. Aplicar configurações de roles via SQL:",
|
|
892
|
-
" ALTER ROLE role_name SET setting_name TO 'value';",
|
|
893
|
-
"4. Habilitar extensões necessárias via Dashboard ou SQL:",
|
|
894
|
-
" CREATE EXTENSION IF NOT EXISTS extension_name;",
|
|
895
|
-
"5. Verificar configurações aplicadas:",
|
|
896
|
-
" SELECT name, setting FROM pg_settings WHERE name IN (...);"
|
|
897
|
-
]
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
};
|
|
901
|
-
|
|
902
|
-
// Salvar arquivo JSON
|
|
903
|
-
const jsonFile = path.join(backupDir, fileName);
|
|
904
|
-
await fs.writeFile(jsonFile, JSON.stringify(result, null, 2));
|
|
905
|
-
|
|
906
|
-
// Limpar arquivo temporário
|
|
907
|
-
await fs.unlink(sqlFile);
|
|
908
|
-
|
|
909
|
-
const stats = await fs.stat(jsonFile);
|
|
910
|
-
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
911
|
-
|
|
912
|
-
console.log(chalk.green(` ✅ Database Settings: ${fileName} (${sizeKB} KB)`));
|
|
913
|
-
|
|
914
|
-
return { success: true, size: sizeKB, fileName: fileName };
|
|
915
|
-
} catch (error) {
|
|
916
|
-
console.log(chalk.yellow(` ⚠️ Erro no backup das Database Settings: ${error.message}`));
|
|
917
|
-
return { success: false };
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
// Backup das Realtime Settings via Captura Interativa
|
|
922
|
-
async function backupRealtimeSettings(projectId, backupDir, skipInteractive = false) {
|
|
923
|
-
try {
|
|
924
|
-
console.log(chalk.gray(' - Capturando Realtime Settings interativamente...'));
|
|
925
|
-
|
|
926
|
-
const result = await captureRealtimeSettings(projectId, backupDir, skipInteractive);
|
|
927
|
-
|
|
928
|
-
const stats = await fs.stat(path.join(backupDir, 'realtime-settings.json'));
|
|
929
|
-
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
930
|
-
|
|
931
|
-
console.log(chalk.green(` ✅ Realtime Settings capturadas: ${sizeKB} KB`));
|
|
932
|
-
|
|
933
|
-
return { success: true, settings: result };
|
|
934
|
-
} catch (error) {
|
|
935
|
-
console.log(chalk.yellow(` ⚠️ Erro ao capturar Realtime Settings: ${error.message}`));
|
|
936
|
-
return { success: false };
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
|