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.
- 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/interactive/envMapper.js +37 -23
- 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
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const { readEnvFile, writeEnvFile, backupEnvFile } = require('../../utils/env');
|
|
6
|
+
const { saveEnvMap } = require('../../utils/envMap');
|
|
7
|
+
const { mapEnvVariablesInteractively } = require('../../interactive/envMapper');
|
|
8
|
+
const { showBetaBanner } = require('../../utils/banner');
|
|
9
|
+
const { listValidBackups, showRestoreSummary } = require('./utils');
|
|
10
|
+
|
|
11
|
+
// Importar todas as etapas
|
|
12
|
+
const step00BackupSelection = require('./steps/00-backup-selection');
|
|
13
|
+
const step01ComponentsSelection = require('./steps/01-components-selection');
|
|
14
|
+
const step02Confirmation = require('./steps/02-confirmation');
|
|
15
|
+
const step03Database = require('./steps/03-database');
|
|
16
|
+
const step04EdgeFunctions = require('./steps/04-edge-functions');
|
|
17
|
+
const step05AuthSettings = require('./steps/05-auth-settings');
|
|
18
|
+
const step06Storage = require('./steps/06-storage');
|
|
19
|
+
const step07DatabaseSettings = require('./steps/07-database-settings');
|
|
20
|
+
const step08RealtimeSettings = require('./steps/08-realtime-settings');
|
|
21
|
+
|
|
22
|
+
module.exports = async (_options) => {
|
|
23
|
+
showBetaBanner();
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Consentimento para leitura e escrita do .env.local
|
|
27
|
+
console.log(chalk.yellow('⚠️ O smoonb irá ler e escrever o arquivo .env.local localmente.'));
|
|
28
|
+
console.log(chalk.yellow(' Um backup automático do .env.local será criado antes de qualquer alteração.'));
|
|
29
|
+
const consent = await inquirer.prompt([{ type: 'confirm', name: 'ok', message: 'Você consente em prosseguir (S/n):', default: true }]);
|
|
30
|
+
if (!consent.ok) {
|
|
31
|
+
console.log(chalk.red('🚫 Operação cancelada pelo usuário.'));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Preparar diretório de processo restore-YYYY-...
|
|
36
|
+
const rootBackupsDir = path.join(process.cwd(), 'backups');
|
|
37
|
+
const now = new Date();
|
|
38
|
+
const ts = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}-${String(now.getHours()).padStart(2,'0')}-${String(now.getMinutes()).padStart(2,'0')}-${String(now.getSeconds()).padStart(2,'0')}`;
|
|
39
|
+
const processDir = path.join(rootBackupsDir, `restore-${ts}`);
|
|
40
|
+
fs.mkdirSync(path.join(processDir, 'env'), { recursive: true });
|
|
41
|
+
|
|
42
|
+
// Backup do .env.local
|
|
43
|
+
const envPath = path.join(process.cwd(), '.env.local');
|
|
44
|
+
const envBackupPath = path.join(processDir, 'env', '.env.local');
|
|
45
|
+
await backupEnvFile(envPath, envBackupPath);
|
|
46
|
+
console.log(chalk.blue(`📁 Backup do .env.local: ${path.relative(process.cwd(), envBackupPath)}`));
|
|
47
|
+
|
|
48
|
+
// Leitura e mapeamento interativo
|
|
49
|
+
const currentEnv = await readEnvFile(envPath);
|
|
50
|
+
const expectedKeys = [
|
|
51
|
+
'NEXT_PUBLIC_SUPABASE_URL',
|
|
52
|
+
'NEXT_PUBLIC_SUPABASE_ANON_KEY',
|
|
53
|
+
'SUPABASE_SERVICE_ROLE_KEY',
|
|
54
|
+
'SUPABASE_DB_URL',
|
|
55
|
+
'SUPABASE_PROJECT_ID',
|
|
56
|
+
'SUPABASE_ACCESS_TOKEN',
|
|
57
|
+
'SMOONB_OUTPUT_DIR'
|
|
58
|
+
];
|
|
59
|
+
const { finalEnv, dePara } = await mapEnvVariablesInteractively(currentEnv, expectedKeys);
|
|
60
|
+
await writeEnvFile(envPath, finalEnv);
|
|
61
|
+
await saveEnvMap(dePara, path.join(processDir, 'env', 'env-map.json'));
|
|
62
|
+
console.log(chalk.green('✅ .env.local atualizado com sucesso. Nenhuma chave renomeada; valores sincronizados.'));
|
|
63
|
+
|
|
64
|
+
// Resolver valores esperados a partir do de-para
|
|
65
|
+
function getValue(expectedKey) {
|
|
66
|
+
const clientKey = Object.keys(dePara).find(k => dePara[k] === expectedKey);
|
|
67
|
+
return clientKey ? finalEnv[clientKey] : '';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Construir targetProject a partir do .env.local mapeado
|
|
71
|
+
const targetProject = {
|
|
72
|
+
targetProjectId: getValue('SUPABASE_PROJECT_ID'),
|
|
73
|
+
targetUrl: getValue('NEXT_PUBLIC_SUPABASE_URL'),
|
|
74
|
+
targetAnonKey: getValue('NEXT_PUBLIC_SUPABASE_ANON_KEY'),
|
|
75
|
+
targetServiceKey: getValue('SUPABASE_SERVICE_ROLE_KEY'),
|
|
76
|
+
targetDatabaseUrl: getValue('SUPABASE_DB_URL'),
|
|
77
|
+
targetAccessToken: getValue('SUPABASE_ACCESS_TOKEN')
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
console.log(chalk.blue(`📁 Buscando backups em: ${getValue('SMOONB_OUTPUT_DIR') || './backups'}`));
|
|
81
|
+
|
|
82
|
+
// 1. Listar backups válidos (.backup.gz)
|
|
83
|
+
const validBackups = await listValidBackups(getValue('SMOONB_OUTPUT_DIR') || './backups');
|
|
84
|
+
|
|
85
|
+
if (validBackups.length === 0) {
|
|
86
|
+
console.error(chalk.red('❌ Nenhum backup válido encontrado'));
|
|
87
|
+
console.log(chalk.yellow('💡 Execute primeiro: npx smoonb backup'));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 2. Selecionar backup interativamente
|
|
92
|
+
const selectedBackup = await step00BackupSelection(validBackups);
|
|
93
|
+
|
|
94
|
+
// 3. Perguntar quais componentes restaurar
|
|
95
|
+
const components = await step01ComponentsSelection(selectedBackup.path);
|
|
96
|
+
|
|
97
|
+
// Validar que pelo menos um componente foi selecionado
|
|
98
|
+
if (!Object.values(components).some(Boolean)) {
|
|
99
|
+
console.error(chalk.red('\n❌ Nenhum componente selecionado para restauração!'));
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 4. Mostrar resumo
|
|
104
|
+
showRestoreSummary(selectedBackup, components, targetProject);
|
|
105
|
+
|
|
106
|
+
// 5. Confirmar execução
|
|
107
|
+
const confirmed = await step02Confirmation();
|
|
108
|
+
if (!confirmed) {
|
|
109
|
+
console.log(chalk.yellow('Restauração cancelada.'));
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 6. Executar restauração
|
|
114
|
+
console.log(chalk.blue('\n🚀 Iniciando restauração...'));
|
|
115
|
+
|
|
116
|
+
// 6.1 Database (se selecionado)
|
|
117
|
+
if (components.database) {
|
|
118
|
+
await step03Database({
|
|
119
|
+
backupFilePath: path.join(selectedBackup.path, selectedBackup.backupFile),
|
|
120
|
+
targetDatabaseUrl: targetProject.targetDatabaseUrl
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 6.2 Edge Functions (se selecionado)
|
|
125
|
+
if (components.edgeFunctions) {
|
|
126
|
+
await step04EdgeFunctions({
|
|
127
|
+
backupPath: selectedBackup.path,
|
|
128
|
+
targetProject
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 6.3 Auth Settings (se selecionado)
|
|
133
|
+
if (components.authSettings) {
|
|
134
|
+
await step05AuthSettings({
|
|
135
|
+
backupPath: selectedBackup.path,
|
|
136
|
+
targetProject
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 6.4 Storage Buckets (se selecionado)
|
|
141
|
+
if (components.storage) {
|
|
142
|
+
await step06Storage({
|
|
143
|
+
backupPath: selectedBackup.path
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 6.5 Database Settings (se selecionado)
|
|
148
|
+
if (components.databaseSettings) {
|
|
149
|
+
await step07DatabaseSettings({
|
|
150
|
+
backupPath: selectedBackup.path,
|
|
151
|
+
targetProject
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 6.6 Realtime Settings (se selecionado)
|
|
156
|
+
if (components.realtimeSettings) {
|
|
157
|
+
await step08RealtimeSettings({
|
|
158
|
+
backupPath: selectedBackup.path,
|
|
159
|
+
targetProject
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// report.json de restauração
|
|
164
|
+
const report = {
|
|
165
|
+
process: 'restore',
|
|
166
|
+
created_at: new Date().toISOString(),
|
|
167
|
+
target_project_id: targetProject.targetProjectId,
|
|
168
|
+
assets: {
|
|
169
|
+
env: path.join(processDir, 'env', '.env.local'),
|
|
170
|
+
env_map: path.join(processDir, 'env', 'env-map.json')
|
|
171
|
+
},
|
|
172
|
+
components: components,
|
|
173
|
+
notes: [
|
|
174
|
+
'supabase/functions limpo antes e depois do deploy (se Edge Functions selecionado)'
|
|
175
|
+
]
|
|
176
|
+
};
|
|
177
|
+
try {
|
|
178
|
+
fs.writeFileSync(path.join(processDir, 'report.json'), JSON.stringify(report, null, 2));
|
|
179
|
+
} catch {
|
|
180
|
+
// silencioso
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log(chalk.green('\n🎉 Restauração completa finalizada!'));
|
|
184
|
+
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error(chalk.red(`❌ Erro na restauração: ${error.message}`));
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const readline = require('readline');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Etapa 0: Seleção interativa de backup
|
|
6
|
+
*/
|
|
7
|
+
module.exports = async (backups) => {
|
|
8
|
+
console.log(chalk.blue('\n📋 Backups disponíveis:'));
|
|
9
|
+
console.log(chalk.blue('═'.repeat(80)));
|
|
10
|
+
|
|
11
|
+
backups.forEach((backup, index) => {
|
|
12
|
+
const date = new Date(backup.created).toLocaleString('pt-BR');
|
|
13
|
+
const projectInfo = backup.projectId !== 'Desconhecido' ? ` (${backup.projectId})` : '';
|
|
14
|
+
|
|
15
|
+
console.log(`${index + 1}. ${backup.name}${projectInfo}`);
|
|
16
|
+
console.log(` 📅 ${date} | 📦 ${backup.size}`);
|
|
17
|
+
console.log('');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const rl = readline.createInterface({
|
|
21
|
+
input: process.stdin,
|
|
22
|
+
output: process.stdout
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const question = (query) => new Promise(resolve => rl.question(query, resolve));
|
|
26
|
+
|
|
27
|
+
const choice = await question(`\nDigite o número do backup para restaurar (1-${backups.length}): `);
|
|
28
|
+
rl.close();
|
|
29
|
+
|
|
30
|
+
const backupIndex = parseInt(choice) - 1;
|
|
31
|
+
|
|
32
|
+
if (backupIndex < 0 || backupIndex >= backups.length) {
|
|
33
|
+
throw new Error('Número inválido');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return backups[backupIndex];
|
|
37
|
+
};
|
|
38
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Etapa 1: Perguntar quais componentes restaurar
|
|
7
|
+
*/
|
|
8
|
+
module.exports = async (backupPath) => {
|
|
9
|
+
const questions = [];
|
|
10
|
+
|
|
11
|
+
// Database
|
|
12
|
+
questions.push({
|
|
13
|
+
type: 'confirm',
|
|
14
|
+
name: 'restoreDatabase',
|
|
15
|
+
message: 'Deseja restaurar Database (S/n):',
|
|
16
|
+
default: true
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Edge Functions
|
|
20
|
+
const edgeFunctionsDir = path.join(backupPath, 'edge-functions');
|
|
21
|
+
if (fs.existsSync(edgeFunctionsDir) && fs.readdirSync(edgeFunctionsDir).length > 0) {
|
|
22
|
+
questions.push({
|
|
23
|
+
type: 'confirm',
|
|
24
|
+
name: 'restoreEdgeFunctions',
|
|
25
|
+
message: 'Deseja restaurar Edge Functions (S/n):',
|
|
26
|
+
default: true
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Auth Settings
|
|
31
|
+
if (fs.existsSync(path.join(backupPath, 'auth-settings.json'))) {
|
|
32
|
+
questions.push({
|
|
33
|
+
type: 'confirm',
|
|
34
|
+
name: 'restoreAuthSettings',
|
|
35
|
+
message: 'Deseja restaurar Auth Settings (s/N):',
|
|
36
|
+
default: false
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Storage Buckets
|
|
41
|
+
const storageDir = path.join(backupPath, 'storage');
|
|
42
|
+
if (fs.existsSync(storageDir) && fs.readdirSync(storageDir).length > 0) {
|
|
43
|
+
questions.push({
|
|
44
|
+
type: 'confirm',
|
|
45
|
+
name: 'restoreStorage',
|
|
46
|
+
message: 'Deseja ver informações de Storage Buckets (s/N):',
|
|
47
|
+
default: false
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Database Extensions and Settings
|
|
52
|
+
const dbSettingsFiles = fs.readdirSync(backupPath)
|
|
53
|
+
.filter(file => file.startsWith('database-settings-') && file.endsWith('.json'));
|
|
54
|
+
if (dbSettingsFiles.length > 0) {
|
|
55
|
+
questions.push({
|
|
56
|
+
type: 'confirm',
|
|
57
|
+
name: 'restoreDatabaseSettings',
|
|
58
|
+
message: 'Deseja restaurar Database Extensions and Settings (s/N):',
|
|
59
|
+
default: false
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Realtime Settings
|
|
64
|
+
if (fs.existsSync(path.join(backupPath, 'realtime-settings.json'))) {
|
|
65
|
+
questions.push({
|
|
66
|
+
type: 'confirm',
|
|
67
|
+
name: 'restoreRealtimeSettings',
|
|
68
|
+
message: 'Deseja restaurar Realtime Settings (s/N):',
|
|
69
|
+
default: false
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const answers = await inquirer.prompt(questions);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
database: answers.restoreDatabase,
|
|
77
|
+
edgeFunctions: answers.restoreEdgeFunctions || false,
|
|
78
|
+
storage: answers.restoreStorage || false,
|
|
79
|
+
authSettings: answers.restoreAuthSettings || false,
|
|
80
|
+
databaseSettings: answers.restoreDatabaseSettings || false,
|
|
81
|
+
realtimeSettings: answers.restoreRealtimeSettings || false
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Etapa 2: Confirmar execução
|
|
5
|
+
*/
|
|
6
|
+
module.exports = async () => {
|
|
7
|
+
const rl = readline.createInterface({
|
|
8
|
+
input: process.stdin,
|
|
9
|
+
output: process.stdout
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const question = (query) => new Promise(resolve => rl.question(query, resolve));
|
|
13
|
+
|
|
14
|
+
const confirm = await question('Deseja continuar com a restauração? (s/N): ');
|
|
15
|
+
rl.close();
|
|
16
|
+
|
|
17
|
+
return confirm.toLowerCase() === 's';
|
|
18
|
+
};
|
|
19
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Etapa 3: Restaurar Database via psql
|
|
7
|
+
*/
|
|
8
|
+
module.exports = async ({ backupFilePath, targetDatabaseUrl }) => {
|
|
9
|
+
console.log(chalk.blue('📊 Restaurando Database...'));
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const backupDirAbs = path.resolve(path.dirname(backupFilePath));
|
|
13
|
+
const fileName = path.basename(backupFilePath);
|
|
14
|
+
let uncompressedFile = fileName;
|
|
15
|
+
|
|
16
|
+
// Verificar se é arquivo .backup.gz (compactado) ou .backup (descompactado)
|
|
17
|
+
if (fileName.endsWith('.backup.gz')) {
|
|
18
|
+
console.log(chalk.gray(' - Arquivo .backup.gz detectado'));
|
|
19
|
+
console.log(chalk.gray(' - Extraindo arquivo .gz...'));
|
|
20
|
+
|
|
21
|
+
const unzipCmd = [
|
|
22
|
+
'docker run --rm',
|
|
23
|
+
`-v "${backupDirAbs}:/host"`,
|
|
24
|
+
'postgres:17 gunzip /host/' + fileName
|
|
25
|
+
].join(' ');
|
|
26
|
+
|
|
27
|
+
execSync(unzipCmd, { stdio: 'pipe' });
|
|
28
|
+
uncompressedFile = fileName.replace('.gz', '');
|
|
29
|
+
console.log(chalk.gray(' - Arquivo descompactado: ' + uncompressedFile));
|
|
30
|
+
} else if (fileName.endsWith('.backup')) {
|
|
31
|
+
console.log(chalk.gray(' - Arquivo .backup detectado (já descompactado)'));
|
|
32
|
+
console.log(chalk.gray(' - Prosseguindo com restauração direta'));
|
|
33
|
+
} else {
|
|
34
|
+
throw new Error(`Formato de arquivo inválido. Esperado .backup.gz ou .backup, recebido: ${fileName}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Extrair credenciais da URL de conexão
|
|
38
|
+
const urlMatch = targetDatabaseUrl.match(/postgresql:\/\/([^@:]+):([^@]+)@(.+)$/);
|
|
39
|
+
|
|
40
|
+
if (!urlMatch) {
|
|
41
|
+
throw new Error('Database URL inválida. Formato esperado: postgresql://user:password@host/database');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Comando psql conforme documentação oficial Supabase
|
|
45
|
+
const restoreCmd = [
|
|
46
|
+
'docker run --rm --network host',
|
|
47
|
+
`-v "${backupDirAbs}:/host"`,
|
|
48
|
+
`-e PGPASSWORD="${encodeURIComponent(urlMatch[2])}"`,
|
|
49
|
+
'postgres:17 psql',
|
|
50
|
+
`-d "${targetDatabaseUrl}"`,
|
|
51
|
+
`-f /host/${uncompressedFile}`
|
|
52
|
+
].join(' ');
|
|
53
|
+
|
|
54
|
+
console.log(chalk.gray(' - Executando psql via Docker...'));
|
|
55
|
+
console.log(chalk.gray(' ℹ️ Seguindo documentação oficial Supabase'));
|
|
56
|
+
console.log(chalk.yellow(' ⚠️ AVISO: Erros como "object already exists" são ESPERADOS'));
|
|
57
|
+
console.log(chalk.yellow(' ⚠️ Isto acontece porque o backup contém CREATE para todos os schemas'));
|
|
58
|
+
console.log(chalk.yellow(' ⚠️ Supabase já tem auth e storage criados, então esses erros são normais'));
|
|
59
|
+
|
|
60
|
+
// Executar comando de restauração
|
|
61
|
+
execSync(restoreCmd, { stdio: 'inherit', encoding: 'utf8' });
|
|
62
|
+
|
|
63
|
+
console.log(chalk.green(' ✅ Database restaurada com sucesso!'));
|
|
64
|
+
console.log(chalk.gray(' ℹ️ Erros "already exists" são normais e não afetam a restauração'));
|
|
65
|
+
|
|
66
|
+
} catch (error) {
|
|
67
|
+
// Erros esperados conforme documentação oficial Supabase
|
|
68
|
+
if (error.message.includes('already exists') ||
|
|
69
|
+
error.message.includes('constraint') ||
|
|
70
|
+
error.message.includes('duplicate') ||
|
|
71
|
+
error.stdout?.includes('already exists')) {
|
|
72
|
+
console.log(chalk.yellow(' ⚠️ Erros esperados encontrados (conforme documentação Supabase)'));
|
|
73
|
+
console.log(chalk.green(' ✅ Database restaurada com sucesso!'));
|
|
74
|
+
console.log(chalk.gray(' ℹ️ Erros são ignorados pois são comandos de CREATE que já existem'));
|
|
75
|
+
} else {
|
|
76
|
+
console.error(chalk.red(` ❌ Erro inesperado na restauração: ${error.message}`));
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs').promises;
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
const { copyDirectoryRecursive } = require('../utils');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Etapa 4: Restaurar Edge Functions via supabase functions deploy
|
|
9
|
+
*/
|
|
10
|
+
module.exports = async ({ backupPath, targetProject }) => {
|
|
11
|
+
console.log(chalk.blue('\n⚡ Restaurando Edge Functions...'));
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const edgeFunctionsDir = path.join(backupPath, 'edge-functions');
|
|
15
|
+
|
|
16
|
+
if (!await fs.access(edgeFunctionsDir).then(() => true).catch(() => false)) {
|
|
17
|
+
console.log(chalk.yellow(' ⚠️ Nenhuma Edge Function encontrada no backup'));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const items = await fs.readdir(edgeFunctionsDir);
|
|
22
|
+
const functions = [];
|
|
23
|
+
|
|
24
|
+
for (const item of items) {
|
|
25
|
+
const itemPath = path.join(edgeFunctionsDir, item);
|
|
26
|
+
const stats = await fs.stat(itemPath);
|
|
27
|
+
if (stats.isDirectory()) {
|
|
28
|
+
functions.push(item);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (functions.length === 0) {
|
|
33
|
+
console.log(chalk.yellow(' ⚠️ Nenhuma Edge Function encontrada no backup'));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(chalk.gray(` - Encontradas ${functions.length} Edge Function(s)`));
|
|
38
|
+
|
|
39
|
+
// COPIAR Edge Functions de backups/backup-XXX/edge-functions para supabase/functions
|
|
40
|
+
const supabaseFunctionsDir = path.join(process.cwd(), 'supabase', 'functions');
|
|
41
|
+
|
|
42
|
+
// Criar diretório supabase/functions se não existir
|
|
43
|
+
await fs.mkdir(supabaseFunctionsDir, { recursive: true });
|
|
44
|
+
|
|
45
|
+
// Limpar supabase/functions antes de copiar
|
|
46
|
+
console.log(chalk.gray(' - Limpando supabase/functions...'));
|
|
47
|
+
try {
|
|
48
|
+
await fs.rm(supabaseFunctionsDir, { recursive: true, force: true });
|
|
49
|
+
await fs.mkdir(supabaseFunctionsDir, { recursive: true });
|
|
50
|
+
} catch (cleanError) {
|
|
51
|
+
// Ignorar erro de limpeza se não existir
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Copiar cada Edge Function para supabase/functions
|
|
55
|
+
for (const funcName of functions) {
|
|
56
|
+
const backupFuncPath = path.join(edgeFunctionsDir, funcName);
|
|
57
|
+
const targetFuncPath = path.join(supabaseFunctionsDir, funcName);
|
|
58
|
+
|
|
59
|
+
console.log(chalk.gray(` - Copiando ${funcName} para supabase/functions...`));
|
|
60
|
+
|
|
61
|
+
// Copiar recursivamente
|
|
62
|
+
await copyDirectoryRecursive(backupFuncPath, targetFuncPath);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(chalk.gray(` - Linkando com projeto ${targetProject.targetProjectId}...`));
|
|
66
|
+
|
|
67
|
+
// Linkar com o projeto destino
|
|
68
|
+
try {
|
|
69
|
+
execSync(`supabase link --project-ref ${targetProject.targetProjectId}`, {
|
|
70
|
+
stdio: 'pipe',
|
|
71
|
+
encoding: 'utf8',
|
|
72
|
+
timeout: 10000,
|
|
73
|
+
env: { ...process.env, SUPABASE_ACCESS_TOKEN: targetProject.targetAccessToken || '' }
|
|
74
|
+
});
|
|
75
|
+
} catch (linkError) {
|
|
76
|
+
console.log(chalk.yellow(' ⚠️ Link pode já existir, continuando...'));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Deploy das Edge Functions
|
|
80
|
+
for (const funcName of functions) {
|
|
81
|
+
console.log(chalk.gray(` - Deployando ${funcName}...`));
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
execSync(`supabase functions deploy ${funcName}`, {
|
|
85
|
+
cwd: process.cwd(),
|
|
86
|
+
stdio: 'pipe',
|
|
87
|
+
encoding: 'utf8',
|
|
88
|
+
timeout: 120000,
|
|
89
|
+
env: { ...process.env, SUPABASE_ACCESS_TOKEN: targetProject.targetAccessToken || '' }
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
console.log(chalk.green(` ✅ ${funcName} deployada com sucesso!`));
|
|
93
|
+
} catch (deployError) {
|
|
94
|
+
console.log(chalk.yellow(` ⚠️ ${funcName} - deploy falhou: ${deployError.message}`));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Limpar supabase/functions após deploy
|
|
99
|
+
console.log(chalk.gray(' - Limpando supabase/functions após deploy...'));
|
|
100
|
+
try {
|
|
101
|
+
await fs.rm(supabaseFunctionsDir, { recursive: true, force: true });
|
|
102
|
+
} catch (cleanError) {
|
|
103
|
+
// Ignorar erro de limpeza
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log(chalk.green(' ✅ Edge Functions restauradas com sucesso!'));
|
|
107
|
+
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(chalk.red(` ❌ Erro ao restaurar Edge Functions: ${error.message}`));
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Etapa 5: Restaurar Auth Settings (interativo - exibir URL e valores)
|
|
8
|
+
*/
|
|
9
|
+
module.exports = async ({ backupPath, targetProject }) => {
|
|
10
|
+
console.log(chalk.blue('\n🔐 Restaurando Auth Settings...'));
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const authSettingsPath = path.join(backupPath, 'auth-settings.json');
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(authSettingsPath)) {
|
|
16
|
+
console.log(chalk.yellow(' ⚠️ Nenhuma configuração de Auth encontrada no backup'));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const authSettings = JSON.parse(fs.readFileSync(authSettingsPath, 'utf8'));
|
|
21
|
+
const dashboardUrl = `https://supabase.com/dashboard/project/${targetProject.targetProjectId}/auth/url-config`;
|
|
22
|
+
|
|
23
|
+
console.log(chalk.green('\n ✅ URL para configuração manual:'));
|
|
24
|
+
console.log(chalk.cyan(` ${dashboardUrl}`));
|
|
25
|
+
console.log(chalk.yellow('\n 📋 Configure manualmente as seguintes opções:'));
|
|
26
|
+
|
|
27
|
+
if (authSettings.settings?.auth_url_config) {
|
|
28
|
+
Object.entries(authSettings.settings.auth_url_config).forEach(([key, value]) => {
|
|
29
|
+
console.log(chalk.gray(` - ${key}: ${value}`));
|
|
30
|
+
});
|
|
31
|
+
} else if (authSettings.auth_url_config) {
|
|
32
|
+
Object.entries(authSettings.auth_url_config).forEach(([key, value]) => {
|
|
33
|
+
console.log(chalk.gray(` - ${key}: ${value}`));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(chalk.yellow('\n ⚠️ Após configurar, pressione Enter para continuar...'));
|
|
38
|
+
|
|
39
|
+
await inquirer.prompt([{
|
|
40
|
+
type: 'input',
|
|
41
|
+
name: 'continue',
|
|
42
|
+
message: 'Pressione Enter para continuar'
|
|
43
|
+
}]);
|
|
44
|
+
|
|
45
|
+
console.log(chalk.green(' ✅ Auth Settings processados'));
|
|
46
|
+
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error(chalk.red(` ❌ Erro ao processar Auth Settings: ${error.message}`));
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Etapa 6: Restaurar Storage Buckets (interativo - exibir informações)
|
|
8
|
+
*/
|
|
9
|
+
module.exports = async ({ backupPath }) => {
|
|
10
|
+
console.log(chalk.blue('\n📦 Restaurando Storage Buckets...'));
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const storageDir = path.join(backupPath, 'storage');
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(storageDir)) {
|
|
16
|
+
console.log(chalk.yellow(' ⚠️ Nenhum bucket de Storage encontrado no backup'));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const manifestPath = path.join(backupPath, 'backup-manifest.json');
|
|
21
|
+
let manifest = null;
|
|
22
|
+
|
|
23
|
+
if (fs.existsSync(manifestPath)) {
|
|
24
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const buckets = manifest?.components?.storage?.buckets || [];
|
|
28
|
+
|
|
29
|
+
if (buckets.length === 0) {
|
|
30
|
+
console.log(chalk.gray(' ℹ️ Nenhum bucket para restaurar'));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log(chalk.green(`\n ✅ ${buckets.length} bucket(s) encontrado(s) no backup`));
|
|
35
|
+
buckets.forEach(bucket => {
|
|
36
|
+
console.log(chalk.gray(` - ${bucket.name} (${bucket.public ? 'público' : 'privado'})`));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const colabUrl = 'https://colab.research.google.com/github/PLyn/supabase-storage-migrate/blob/main/Supabase_Storage_migration.ipynb';
|
|
40
|
+
|
|
41
|
+
console.log(chalk.yellow('\n ⚠️ Migração de objetos de Storage requer processo manual'));
|
|
42
|
+
console.log(chalk.cyan(` ℹ️ Use o script do Google Colab: ${colabUrl}`));
|
|
43
|
+
console.log(chalk.gray('\n 📋 Instruções:'));
|
|
44
|
+
console.log(chalk.gray(' 1. Execute o script no Google Colab'));
|
|
45
|
+
console.log(chalk.gray(' 2. Configure as credenciais dos projetos (origem e destino)'));
|
|
46
|
+
console.log(chalk.gray(' 3. Execute a migração'));
|
|
47
|
+
|
|
48
|
+
await inquirer.prompt([{
|
|
49
|
+
type: 'input',
|
|
50
|
+
name: 'continue',
|
|
51
|
+
message: 'Pressione Enter para continuar'
|
|
52
|
+
}]);
|
|
53
|
+
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(chalk.red(` ❌ Erro ao processar Storage: ${error.message}`));
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|