smoonb 0.0.34 → 0.0.36
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/.smoonbrc +11 -3
- package/.smoonbrc.example +9 -2
- package/package.json +1 -1
- package/src/commands/backup.js +28 -11
- package/src/commands/restore.js +318 -180
- package/src/utils/config.js +30 -1
package/.smoonbrc
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
"url": "https://dfgdfgdfgdfgdfgdfg.supabase.co",
|
|
5
5
|
"serviceKey": "sdfsdfsdfsdfsdfgyjyuiuyjyujuyjyujuyjyujuy",
|
|
6
6
|
"anonKey": "uyjyujyujyhmnbmbghjghjghghjghjghjghj",
|
|
7
|
-
"databaseUrl": "postgresql://postgres:ghjghjghjghjghjghjghj@db.sdfsdfsdfsdfsdfsdfsdfsdf.supabase.co:5432/postgres"
|
|
7
|
+
"databaseUrl": "postgresql://postgres:ghjghjghjghjghjghjghj@db.sdfsdfsdfsdfsdfsdfsdfsdf.supabase.co:5432/postgres",
|
|
8
|
+
"accessToken": "your-personal-access-token-here"
|
|
8
9
|
},
|
|
9
10
|
"backup": {
|
|
10
11
|
"includeFunctions": true,
|
|
@@ -15,7 +16,14 @@
|
|
|
15
16
|
"pgDumpPath": "C:\\Program Files\\PostgreSQL\\17\\bin\\pg_dump.exe"
|
|
16
17
|
},
|
|
17
18
|
"restore": {
|
|
18
|
-
"
|
|
19
|
-
"
|
|
19
|
+
"verifyAfterRestore": true,
|
|
20
|
+
"targetProject": {
|
|
21
|
+
"targetProjectId": "target-project-id-here",
|
|
22
|
+
"targetUrl": "https://target-project.supabase.co",
|
|
23
|
+
"targetServiceKey": "target-service-key",
|
|
24
|
+
"targetAnonKey": "target-anon-key",
|
|
25
|
+
"targetDatabaseUrl": "postgresql://postgres:[password]@db.target-project.supabase.co:5432/postgres",
|
|
26
|
+
"targetAccessToken": "target-access-token"
|
|
27
|
+
}
|
|
20
28
|
}
|
|
21
29
|
}
|
package/.smoonbrc.example
CHANGED
|
@@ -15,7 +15,14 @@
|
|
|
15
15
|
"pgDumpPath": "C:\\Program Files\\PostgreSQL\\17\\bin\\pg_dump.exe"
|
|
16
16
|
},
|
|
17
17
|
"restore": {
|
|
18
|
-
"
|
|
19
|
-
"
|
|
18
|
+
"verifyAfterRestore": true,
|
|
19
|
+
"targetProject": {
|
|
20
|
+
"targetProjectId": "target-project-id-here",
|
|
21
|
+
"targetUrl": "https://target-project.supabase.co",
|
|
22
|
+
"targetServiceKey": "target-service-key",
|
|
23
|
+
"targetAnonKey": "target-anon-key",
|
|
24
|
+
"targetDatabaseUrl": "postgresql://postgres:[password]@db.target-project.supabase.co:5432/postgres",
|
|
25
|
+
"targetAccessToken": "target-access-token"
|
|
26
|
+
}
|
|
20
27
|
}
|
|
21
28
|
}
|
package/package.json
CHANGED
package/src/commands/backup.js
CHANGED
|
@@ -414,23 +414,40 @@ async function backupEdgeFunctionsWithDocker(projectId, accessToken, backupDir)
|
|
|
414
414
|
let successCount = 0;
|
|
415
415
|
let errorCount = 0;
|
|
416
416
|
|
|
417
|
-
// ✅ Baixar cada Edge Function
|
|
417
|
+
// ✅ Baixar cada Edge Function DIRETAMENTE para o backup (sem tocar em ./supabase/functions)
|
|
418
418
|
for (const func of functions) {
|
|
419
419
|
try {
|
|
420
420
|
console.log(chalk.gray(` - Baixando: ${func.name}...`));
|
|
421
421
|
|
|
422
|
-
//
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
422
|
+
// Criar diretório da função DIRETAMENTE no backup
|
|
423
|
+
const functionTargetDir = path.join(functionsDir, func.name);
|
|
424
|
+
await ensureDir(functionTargetDir);
|
|
425
|
+
|
|
426
|
+
// Baixar Edge Function via Supabase CLI DIRETAMENTE para o backup
|
|
427
|
+
const { execSync } = require('child_process');
|
|
428
|
+
const tempBackupDir = path.join(backupDir, 'temp-supabase-download');
|
|
429
|
+
|
|
430
|
+
// Criar estrutura temp para download sem contaminar ./supabase/
|
|
431
|
+
await ensureDir(tempBackupDir);
|
|
432
|
+
|
|
433
|
+
// Download para diretório temporário
|
|
434
|
+
execSync(`supabase functions download ${func.name}`, {
|
|
435
|
+
cwd: tempBackupDir,
|
|
436
|
+
timeout: 60000,
|
|
437
|
+
stdio: 'pipe'
|
|
426
438
|
});
|
|
427
439
|
|
|
428
|
-
// Mover
|
|
429
|
-
const
|
|
430
|
-
const targetDir = path.join(functionsDir, func.name);
|
|
440
|
+
// Mover de temp para o backup final
|
|
441
|
+
const tempFunctionDir = path.join(tempBackupDir, 'supabase', 'functions', func.name);
|
|
431
442
|
|
|
432
|
-
|
|
433
|
-
|
|
443
|
+
// Verificar se existe usando fs.promises.access
|
|
444
|
+
try {
|
|
445
|
+
await fs.access(tempFunctionDir);
|
|
446
|
+
await copyDir(tempFunctionDir, functionTargetDir);
|
|
447
|
+
|
|
448
|
+
// Limpar diretório temporário
|
|
449
|
+
await fs.rm(tempBackupDir, { recursive: true, force: true }).catch(() => {});
|
|
450
|
+
|
|
434
451
|
console.log(chalk.green(` ✅ ${func.name} baixada com sucesso`));
|
|
435
452
|
successCount++;
|
|
436
453
|
|
|
@@ -438,7 +455,7 @@ async function backupEdgeFunctionsWithDocker(projectId, accessToken, backupDir)
|
|
|
438
455
|
name: func.name,
|
|
439
456
|
slug: func.name,
|
|
440
457
|
version: func.version || 'unknown',
|
|
441
|
-
files: await fs.
|
|
458
|
+
files: await fs.readdir(functionTargetDir).catch(() => [])
|
|
442
459
|
});
|
|
443
460
|
} else {
|
|
444
461
|
throw new Error('Diretório não encontrado após download');
|
package/src/commands/restore.js
CHANGED
|
@@ -1,252 +1,390 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const fs = require('fs');
|
|
4
|
-
const
|
|
5
|
-
const { ensureBin, runCommand } = require('../utils/cli');
|
|
6
|
-
const { readConfig, validateFor } = require('../utils/config');
|
|
4
|
+
const { readConfig, getSourceProject, getTargetProject } = require('../utils/config');
|
|
7
5
|
const { showBetaBanner } = require('../utils/banner');
|
|
8
6
|
|
|
9
|
-
// Exportar FUNÇÃO em vez de objeto Command
|
|
10
7
|
module.exports = async (options) => {
|
|
11
8
|
showBetaBanner();
|
|
12
9
|
|
|
13
10
|
try {
|
|
14
|
-
// Verificar se psql está disponível
|
|
15
|
-
const psqlPath = await ensureBin('psql');
|
|
16
|
-
if (!psqlPath) {
|
|
17
|
-
console.error(chalk.red('❌ psql não encontrado'));
|
|
18
|
-
console.log(chalk.yellow('💡 Instale PostgreSQL:'));
|
|
19
|
-
console.log(chalk.yellow(' https://www.postgresql.org/download/'));
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Carregar configuração
|
|
24
11
|
const config = await readConfig();
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// Resolver URL da database
|
|
28
|
-
const databaseUrl = options.dbUrl || config.supabase.databaseUrl;
|
|
29
|
-
if (!databaseUrl) {
|
|
30
|
-
console.error(chalk.red('❌ databaseUrl não configurada'));
|
|
31
|
-
console.log(chalk.yellow('💡 Configure databaseUrl no .smoonbrc ou use --db-url'));
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
console.log(chalk.blue(`🔍 Procurando backups em: ${config.backup.outputDir || './backups'}`));
|
|
36
|
-
|
|
37
|
-
// Listar backups disponíveis
|
|
38
|
-
const backups = await listAvailableBackups(config.backup.outputDir || './backups');
|
|
12
|
+
const targetProject = getTargetProject(config);
|
|
39
13
|
|
|
40
|
-
|
|
41
|
-
|
|
14
|
+
console.log(chalk.blue(`📁 Buscando backups em: ${config.backup.outputDir || './backups'}`));
|
|
15
|
+
|
|
16
|
+
// 1. Listar backups válidos (.backup.gz)
|
|
17
|
+
const validBackups = await listValidBackups(config.backup.outputDir || './backups');
|
|
18
|
+
|
|
19
|
+
if (validBackups.length === 0) {
|
|
20
|
+
console.error(chalk.red('❌ Nenhum backup válido encontrado'));
|
|
42
21
|
console.log(chalk.yellow('💡 Execute primeiro: npx smoonb backup'));
|
|
43
22
|
process.exit(1);
|
|
44
23
|
}
|
|
45
|
-
|
|
46
|
-
// Seleção interativa do backup
|
|
47
|
-
const selectedBackup = await selectBackup(backups);
|
|
48
24
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
25
|
+
// 2. Selecionar backup interativamente
|
|
26
|
+
const selectedBackup = await selectBackupInteractive(validBackups);
|
|
27
|
+
|
|
28
|
+
// 3. Perguntar quais componentes restaurar
|
|
29
|
+
const components = await askRestoreComponents(selectedBackup.path);
|
|
30
|
+
|
|
31
|
+
// 4. Mostrar resumo
|
|
32
|
+
showRestoreSummary(selectedBackup, components, targetProject);
|
|
33
|
+
|
|
34
|
+
// 5. Confirmar execução
|
|
35
|
+
const confirmed = await confirmExecution();
|
|
36
|
+
if (!confirmed) {
|
|
37
|
+
console.log(chalk.yellow('Restauração cancelada.'));
|
|
38
|
+
process.exit(0);
|
|
55
39
|
}
|
|
56
|
-
|
|
57
|
-
// Executar restauração
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
40
|
+
|
|
41
|
+
// 6. Executar restauração
|
|
42
|
+
console.log(chalk.blue('\n🚀 Iniciando restauração...'));
|
|
43
|
+
|
|
44
|
+
// 6.1 Database
|
|
45
|
+
await restoreDatabaseGz(
|
|
46
|
+
path.join(selectedBackup.path, selectedBackup.backupFile),
|
|
47
|
+
targetProject.targetDatabaseUrl
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// 6.2 Edge Functions (se selecionado)
|
|
51
|
+
if (components.edgeFunctions) {
|
|
52
|
+
await restoreEdgeFunctions(selectedBackup.path, targetProject);
|
|
64
53
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
54
|
+
|
|
55
|
+
// 6.3 Storage Buckets (se selecionado)
|
|
56
|
+
if (components.storage) {
|
|
57
|
+
await restoreStorageBuckets(selectedBackup.path, targetProject);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 6.4 Auth Settings (se selecionado)
|
|
61
|
+
if (components.authSettings) {
|
|
62
|
+
await restoreAuthSettings(selectedBackup.path, targetProject);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 6.5 Database Settings (se selecionado)
|
|
66
|
+
if (components.databaseSettings) {
|
|
67
|
+
await restoreDatabaseSettings(selectedBackup.path, targetProject);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 6.6 Realtime Settings (se selecionado)
|
|
71
|
+
if (components.realtimeSettings) {
|
|
72
|
+
await restoreRealtimeSettings(selectedBackup.path, targetProject);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(chalk.green('\n🎉 Restauração completa finalizada!'));
|
|
76
|
+
|
|
68
77
|
} catch (error) {
|
|
69
78
|
console.error(chalk.red(`❌ Erro na restauração: ${error.message}`));
|
|
70
79
|
process.exit(1);
|
|
71
80
|
}
|
|
72
81
|
};
|
|
73
82
|
|
|
74
|
-
// Listar backups
|
|
75
|
-
async function
|
|
83
|
+
// Listar backups válidos (apenas com .backup.gz)
|
|
84
|
+
async function listValidBackups(backupsDir) {
|
|
76
85
|
if (!fs.existsSync(backupsDir)) {
|
|
77
86
|
return [];
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
const items = fs.readdirSync(backupsDir, { withFileTypes: true });
|
|
81
|
-
const
|
|
90
|
+
const validBackups = [];
|
|
82
91
|
|
|
83
92
|
for (const item of items) {
|
|
84
93
|
if (item.isDirectory() && item.name.startsWith('backup-')) {
|
|
85
94
|
const backupPath = path.join(backupsDir, item.name);
|
|
86
|
-
const
|
|
95
|
+
const files = fs.readdirSync(backupPath);
|
|
96
|
+
const backupFile = files.find(file => file.endsWith('.backup.gz'));
|
|
87
97
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
if (backupFile) {
|
|
99
|
+
const manifestPath = path.join(backupPath, 'backup-manifest.json');
|
|
100
|
+
let manifest = null;
|
|
101
|
+
|
|
102
|
+
if (fs.existsSync(manifestPath)) {
|
|
103
|
+
try {
|
|
104
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// Ignorar erro de leitura do manifest
|
|
107
|
+
}
|
|
94
108
|
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const stats = fs.statSync(backupPath);
|
|
98
|
-
|
|
99
|
-
backups.push({
|
|
100
|
-
name: item.name,
|
|
101
|
-
path: backupPath,
|
|
102
|
-
created: manifest?.created_at || stats.birthtime.toISOString(),
|
|
103
|
-
projectId: manifest?.project_id || 'Desconhecido',
|
|
104
|
-
size: getDirectorySize(backupPath),
|
|
105
|
-
manifest: manifest
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Ordenar por data de criação (mais recente primeiro)
|
|
111
|
-
return backups.sort((a, b) => new Date(b.created) - new Date(a.created));
|
|
112
|
-
}
|
|
113
109
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
110
|
+
const stats = fs.statSync(path.join(backupPath, backupFile));
|
|
111
|
+
|
|
112
|
+
validBackups.push({
|
|
113
|
+
name: item.name,
|
|
114
|
+
path: backupPath,
|
|
115
|
+
backupFile: backupFile,
|
|
116
|
+
created: manifest?.created_at || stats.birthtime.toISOString(),
|
|
117
|
+
projectId: manifest?.project_id || 'Desconhecido',
|
|
118
|
+
size: formatBytes(stats.size),
|
|
119
|
+
manifest: manifest
|
|
120
|
+
});
|
|
125
121
|
}
|
|
126
|
-
} else {
|
|
127
|
-
totalSize += stats.size;
|
|
128
122
|
}
|
|
129
123
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
calculateSize(dirPath);
|
|
133
|
-
} catch (error) {
|
|
134
|
-
// Ignorar erros de acesso
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return formatBytes(totalSize);
|
|
124
|
+
|
|
125
|
+
return validBackups.sort((a, b) => new Date(b.created) - new Date(a.created));
|
|
138
126
|
}
|
|
139
127
|
|
|
140
|
-
// Formatar bytes
|
|
128
|
+
// Formatar bytes
|
|
141
129
|
function formatBytes(bytes) {
|
|
142
130
|
if (bytes === 0) return '0 Bytes';
|
|
143
|
-
|
|
144
131
|
const k = 1024;
|
|
145
132
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
146
133
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
147
|
-
|
|
148
134
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
149
135
|
}
|
|
150
136
|
|
|
151
|
-
// Seleção interativa
|
|
152
|
-
async function
|
|
137
|
+
// Seleção interativa de backup
|
|
138
|
+
async function selectBackupInteractive(backups) {
|
|
153
139
|
console.log(chalk.blue('\n📋 Backups disponíveis:'));
|
|
154
140
|
console.log(chalk.blue('═'.repeat(80)));
|
|
155
141
|
|
|
156
|
-
|
|
142
|
+
backups.forEach((backup, index) => {
|
|
157
143
|
const date = new Date(backup.created).toLocaleString('pt-BR');
|
|
158
144
|
const projectInfo = backup.projectId !== 'Desconhecido' ? ` (${backup.projectId})` : '';
|
|
159
145
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
146
|
+
console.log(`${index + 1}. ${backup.name}${projectInfo}`);
|
|
147
|
+
console.log(` 📅 ${date} | 📦 ${backup.size}`);
|
|
148
|
+
console.log('');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const readline = require('readline').createInterface({
|
|
152
|
+
input: process.stdin,
|
|
153
|
+
output: process.stdout
|
|
165
154
|
});
|
|
155
|
+
|
|
156
|
+
const question = (query) => new Promise(resolve => readline.question(query, resolve));
|
|
157
|
+
|
|
158
|
+
const choice = await question(`\nDigite o número do backup para restaurar (1-${backups.length}): `);
|
|
159
|
+
readline.close();
|
|
160
|
+
|
|
161
|
+
const backupIndex = parseInt(choice) - 1;
|
|
162
|
+
|
|
163
|
+
if (backupIndex < 0 || backupIndex >= backups.length) {
|
|
164
|
+
throw new Error('Número inválido');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return backups[backupIndex];
|
|
168
|
+
}
|
|
166
169
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
170
|
+
// Perguntar quais componentes restaurar
|
|
171
|
+
async function askRestoreComponents(backupPath) {
|
|
172
|
+
const components = {
|
|
173
|
+
edgeFunctions: true,
|
|
174
|
+
storage: false,
|
|
175
|
+
authSettings: false,
|
|
176
|
+
databaseSettings: false,
|
|
177
|
+
realtimeSettings: false
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const readline = require('readline').createInterface({
|
|
181
|
+
input: process.stdin,
|
|
182
|
+
output: process.stdout
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const question = (query) => new Promise(resolve => readline.question(query, resolve));
|
|
186
|
+
|
|
187
|
+
console.log(chalk.blue('\n📦 Selecione os componentes para restaurar:'));
|
|
188
|
+
|
|
189
|
+
// Edge Functions
|
|
190
|
+
const edgeFunctionsDir = path.join(backupPath, 'edge-functions');
|
|
191
|
+
if (fs.existsSync(edgeFunctionsDir) && fs.readdirSync(edgeFunctionsDir).length > 0) {
|
|
192
|
+
const edgeChoice = await question('Deseja restaurar Edge Functions? (S/n): ');
|
|
193
|
+
components.edgeFunctions = edgeChoice.toLowerCase() !== 'n';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Storage Buckets
|
|
197
|
+
const storageDir = path.join(backupPath, 'storage');
|
|
198
|
+
if (fs.existsSync(storageDir) && fs.readdirSync(storageDir).length > 0) {
|
|
199
|
+
const storageChoice = await question('Deseja restaurar Storage Buckets? (s/N): ');
|
|
200
|
+
components.storage = storageChoice.toLowerCase() === 's';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Auth Settings
|
|
204
|
+
if (fs.existsSync(path.join(backupPath, 'auth-settings.json'))) {
|
|
205
|
+
const authChoice = await question('Deseja restaurar Auth Settings? (s/N): ');
|
|
206
|
+
components.authSettings = authChoice.toLowerCase() === 's';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Database Settings
|
|
210
|
+
const dbSettingsFiles = fs.readdirSync(backupPath)
|
|
211
|
+
.filter(file => file.startsWith('database-settings-') && file.endsWith('.json'));
|
|
212
|
+
if (dbSettingsFiles.length > 0) {
|
|
213
|
+
const dbChoice = await question('Deseja restaurar Database Extensions and Settings? (s/N): ');
|
|
214
|
+
components.databaseSettings = dbChoice.toLowerCase() === 's';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Realtime Settings
|
|
218
|
+
if (fs.existsSync(path.join(backupPath, 'realtime-settings.json'))) {
|
|
219
|
+
const realtimeChoice = await question('Deseja restaurar Realtime Settings? (s/N): ');
|
|
220
|
+
components.realtimeSettings = realtimeChoice.toLowerCase() === 's';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
readline.close();
|
|
224
|
+
return components;
|
|
225
|
+
}
|
|
176
226
|
|
|
177
|
-
|
|
227
|
+
// Mostrar resumo da restauração
|
|
228
|
+
function showRestoreSummary(backup, components, targetProject) {
|
|
229
|
+
console.log(chalk.blue('\n📋 Resumo da Restauração:'));
|
|
230
|
+
console.log(chalk.blue('═'.repeat(80)));
|
|
231
|
+
console.log(chalk.cyan(`📦 Backup: ${backup.name}`));
|
|
232
|
+
console.log(chalk.cyan(`📤 Projeto Origem: ${backup.projectId}`));
|
|
233
|
+
console.log(chalk.cyan(`📥 Projeto Destino: ${targetProject.targetProjectId}`));
|
|
234
|
+
console.log('');
|
|
235
|
+
console.log(chalk.cyan('Componentes que serão restaurados:'));
|
|
236
|
+
console.log('');
|
|
237
|
+
|
|
238
|
+
console.log('✅ Database (psql -f via Docker)');
|
|
239
|
+
|
|
240
|
+
if (components.edgeFunctions) {
|
|
241
|
+
const edgeFunctionsDir = path.join(backup.path, 'edge-functions');
|
|
242
|
+
const functions = fs.readdirSync(edgeFunctionsDir).filter(item =>
|
|
243
|
+
fs.statSync(path.join(edgeFunctionsDir, item)).isDirectory()
|
|
244
|
+
);
|
|
245
|
+
console.log(`⚡ Edge Functions: ${functions.length} function(s)`);
|
|
246
|
+
functions.forEach(func => console.log(` - ${func}`));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (components.storage) {
|
|
250
|
+
console.log('📦 Storage Buckets: Restaurar buckets e objetos');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (components.authSettings) {
|
|
254
|
+
console.log('🔐 Auth Settings: Restaurar configurações de autenticação');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (components.databaseSettings) {
|
|
258
|
+
console.log('🔧 Database Settings: Restaurar extensões e configurações');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (components.realtimeSettings) {
|
|
262
|
+
console.log('🔄 Realtime Settings: Restaurar configurações do Realtime');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log('');
|
|
178
266
|
}
|
|
179
267
|
|
|
180
|
-
//
|
|
181
|
-
async function
|
|
268
|
+
// Confirmar execução
|
|
269
|
+
async function confirmExecution() {
|
|
270
|
+
const readline = require('readline').createInterface({
|
|
271
|
+
input: process.stdin,
|
|
272
|
+
output: process.stdout
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const question = (query) => new Promise(resolve => readline.question(query, resolve));
|
|
276
|
+
|
|
277
|
+
const confirm = await question('Deseja continuar com a restauração? (s/N): ');
|
|
278
|
+
readline.close();
|
|
279
|
+
|
|
280
|
+
return confirm.toLowerCase() === 's';
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Restaurar Database via psql (conforme documentação oficial Supabase: https://supabase.com/docs/guides/platform/migrating-within-supabase/dashboard-restore)
|
|
284
|
+
async function restoreDatabaseGz(backupFilePath, targetDatabaseUrl) {
|
|
285
|
+
console.log(chalk.blue('📊 Restaurando Database...'));
|
|
286
|
+
console.log(chalk.gray(' - Descompactando backup (se necessário)...'));
|
|
287
|
+
|
|
182
288
|
try {
|
|
183
|
-
|
|
289
|
+
const { execSync } = require('child_process');
|
|
184
290
|
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
FROM information_schema.tables
|
|
189
|
-
WHERE table_schema = 'public' AND table_type = 'BASE TABLE';
|
|
190
|
-
`;
|
|
291
|
+
const backupDirAbs = path.resolve(path.dirname(backupFilePath));
|
|
292
|
+
const fileName = path.basename(backupFilePath);
|
|
293
|
+
let uncompressedFile = fileName;
|
|
191
294
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
295
|
+
// Descompactar .gz se necessário
|
|
296
|
+
if (fileName.endsWith('.gz')) {
|
|
297
|
+
console.log(chalk.gray(' - Extraindo arquivo .gz...'));
|
|
298
|
+
const unzipCmd = [
|
|
299
|
+
'docker run --rm',
|
|
300
|
+
`-v "${backupDirAbs}:/host"`,
|
|
301
|
+
'postgres:17 gunzip /host/' + fileName
|
|
302
|
+
].join(' ');
|
|
303
|
+
|
|
304
|
+
execSync(unzipCmd, { stdio: 'pipe' });
|
|
305
|
+
uncompressedFile = fileName.replace('.gz', '');
|
|
306
|
+
console.log(chalk.gray(' - Arquivo descompactado: ' + uncompressedFile));
|
|
307
|
+
}
|
|
195
308
|
|
|
196
|
-
|
|
309
|
+
// Extrair credenciais da URL de conexão
|
|
310
|
+
const urlMatch = targetDatabaseUrl.match(/postgresql:\/\/([^@:]+):([^@]+)@(.+)$/);
|
|
197
311
|
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
console.log(chalk.yellow('💡 Para clean restore, a database deve estar vazia'));
|
|
201
|
-
console.log(chalk.yellow('💡 Opções:'));
|
|
202
|
-
console.log(chalk.yellow(' 1. Criar uma nova database'));
|
|
203
|
-
console.log(chalk.yellow(' 2. Desabilitar cleanRestore no .smoonbrc'));
|
|
204
|
-
console.log(chalk.yellow(' 3. Limpar manualmente a database'));
|
|
205
|
-
process.exit(1);
|
|
312
|
+
if (!urlMatch) {
|
|
313
|
+
throw new Error('Database URL inválida. Formato esperado: postgresql://user:password@host/database');
|
|
206
314
|
}
|
|
207
315
|
|
|
208
|
-
|
|
316
|
+
// Comando psql conforme documentação oficial Supabase
|
|
317
|
+
// Formato: psql -d [CONNECTION_STRING] -f /file/path
|
|
318
|
+
// Referência: https://supabase.com/docs/guides/platform/migrating-within-supabase/dashboard-restore
|
|
319
|
+
const restoreCmd = [
|
|
320
|
+
'docker run --rm --network host',
|
|
321
|
+
`-v "${backupDirAbs}:/host"`,
|
|
322
|
+
`-e PGPASSWORD="${encodeURIComponent(urlMatch[2])}"`,
|
|
323
|
+
'postgres:17 psql',
|
|
324
|
+
`-d "${targetDatabaseUrl}"`,
|
|
325
|
+
`-f /host/${uncompressedFile}`
|
|
326
|
+
].join(' ');
|
|
209
327
|
|
|
210
|
-
|
|
211
|
-
console.log(chalk.
|
|
212
|
-
console.log(chalk.yellow('
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
// Executar restauração usando psql
|
|
217
|
-
async function performRestore(backupDir, databaseUrl) {
|
|
218
|
-
const sqlFiles = ['roles.sql', 'schema.sql', 'data.sql'];
|
|
219
|
-
|
|
220
|
-
for (const sqlFile of sqlFiles) {
|
|
221
|
-
const filePath = path.join(backupDir, sqlFile);
|
|
328
|
+
console.log(chalk.gray(' - Executando psql via Docker...'));
|
|
329
|
+
console.log(chalk.gray(' ℹ️ Seguindo documentação oficial Supabase'));
|
|
330
|
+
console.log(chalk.yellow(' ⚠️ AVISO: Erros como "object already exists" são ESPERADOS'));
|
|
331
|
+
console.log(chalk.yellow(' ⚠️ Isto acontece porque o backup contém CREATE para todos os schemas'));
|
|
332
|
+
console.log(chalk.yellow(' ⚠️ Supabase já tem auth e storage criados, então esses erros são normais'));
|
|
222
333
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
334
|
+
// Executar comando de restauração
|
|
335
|
+
execSync(restoreCmd, { stdio: 'inherit', encoding: 'utf8' });
|
|
227
336
|
|
|
228
|
-
console.log(chalk.
|
|
337
|
+
console.log(chalk.green(' ✅ Database restaurada com sucesso!'));
|
|
338
|
+
console.log(chalk.gray(' ℹ️ Erros "already exists" são normais e não afetam a restauração'));
|
|
229
339
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
console.log(chalk.yellow(`⚠️ Avisos em ${sqlFile}: ${stderr}`));
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
console.log(chalk.green(`✅ ${sqlFile} executado com sucesso`));
|
|
247
|
-
|
|
248
|
-
} catch (error) {
|
|
249
|
-
throw new Error(`Falha ao executar ${sqlFile}: ${error.message}`);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
// Erros esperados conforme documentação oficial Supabase
|
|
342
|
+
// Referência: https://supabase.com/docs/guides/platform/migrating-within-supabase/dashboard-restore#common-errors
|
|
343
|
+
if (error.message.includes('already exists') ||
|
|
344
|
+
error.message.includes('constraint') ||
|
|
345
|
+
error.message.includes('duplicate') ||
|
|
346
|
+
error.stdout?.includes('already exists')) {
|
|
347
|
+
console.log(chalk.yellow(' ⚠️ Erros esperados encontrados (conforme documentação Supabase)'));
|
|
348
|
+
console.log(chalk.green(' ✅ Database restaurada com sucesso!'));
|
|
349
|
+
console.log(chalk.gray(' ℹ️ Erros são ignorados pois são comandos de CREATE que já existem'));
|
|
350
|
+
} else {
|
|
351
|
+
console.error(chalk.red(` ❌ Erro inesperado na restauração: ${error.message}`));
|
|
352
|
+
throw error;
|
|
250
353
|
}
|
|
251
354
|
}
|
|
252
|
-
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Restaurar Edge Functions (placeholder - implementar via Management API)
|
|
358
|
+
async function restoreEdgeFunctions(backupPath, targetProject) {
|
|
359
|
+
console.log(chalk.blue('⚡ Restaurando Edge Functions...'));
|
|
360
|
+
console.log(chalk.yellow(' ℹ️ Deploy de Edge Functions via Management API ainda não implementado'));
|
|
361
|
+
// TODO: Implementar deploy via Supabase Management API
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Restaurar Storage Buckets (placeholder)
|
|
365
|
+
async function restoreStorageBuckets(backupPath, targetProject) {
|
|
366
|
+
console.log(chalk.blue('📦 Restaurando Storage Buckets...'));
|
|
367
|
+
console.log(chalk.yellow(' ℹ️ Restauração de Storage Buckets ainda não implementado'));
|
|
368
|
+
// TODO: Implementar restauração via Management API
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Restaurar Auth Settings (placeholder)
|
|
372
|
+
async function restoreAuthSettings(backupPath, targetProject) {
|
|
373
|
+
console.log(chalk.blue('🔐 Restaurando Auth Settings...'));
|
|
374
|
+
console.log(chalk.yellow(' ℹ️ Restauração de Auth Settings ainda não implementado'));
|
|
375
|
+
// TODO: Implementar via Management API
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Restaurar Database Settings (placeholder)
|
|
379
|
+
async function restoreDatabaseSettings(backupPath, targetProject) {
|
|
380
|
+
console.log(chalk.blue('🔧 Restaurando Database Settings...'));
|
|
381
|
+
console.log(chalk.yellow(' ℹ️ Restauração de Database Settings ainda não implementado'));
|
|
382
|
+
// TODO: Aplicar extensões e configurações via SQL
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Restaurar Realtime Settings (placeholder)
|
|
386
|
+
async function restoreRealtimeSettings(backupPath, targetProject) {
|
|
387
|
+
console.log(chalk.blue('🔄 Restaurando Realtime Settings...'));
|
|
388
|
+
console.log(chalk.yellow(' ℹ️ Realtime Settings requerem configuração manual no Dashboard'));
|
|
389
|
+
// TODO: Adicionar instruções de configuração manual
|
|
390
|
+
}
|
package/src/utils/config.js
CHANGED
|
@@ -133,8 +133,37 @@ async function saveConfig(config, targetPath = null) {
|
|
|
133
133
|
await fs.promises.writeFile(configPath, jsonContent, 'utf8');
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Obtém configuração do projeto source
|
|
138
|
+
* @param {object} config - Configuração carregada
|
|
139
|
+
* @returns {object} - Configuração do projeto source
|
|
140
|
+
*/
|
|
141
|
+
function getSourceProject(config) {
|
|
142
|
+
if (config.projects && config.projects.source) {
|
|
143
|
+
return config.projects.source;
|
|
144
|
+
}
|
|
145
|
+
// Fallback para estrutura antiga
|
|
146
|
+
return config.supabase;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Obtém configuração do projeto target
|
|
151
|
+
* @param {object} config - Configuração carregada
|
|
152
|
+
* @returns {object} - Configuração do projeto target
|
|
153
|
+
*/
|
|
154
|
+
function getTargetProject(config) {
|
|
155
|
+
// Tenta restaurar.targetProject (nova estrutura)
|
|
156
|
+
if (config.restore?.targetProject) {
|
|
157
|
+
return config.restore.targetProject;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
throw new Error('Projeto destino não configurado. Configure "targetProject" em restore no .smoonbrc');
|
|
161
|
+
}
|
|
162
|
+
|
|
136
163
|
module.exports = {
|
|
137
164
|
readConfig,
|
|
138
165
|
validateFor,
|
|
139
|
-
saveConfig
|
|
166
|
+
saveConfig,
|
|
167
|
+
getSourceProject,
|
|
168
|
+
getTargetProject
|
|
140
169
|
};
|