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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Etapa 7: Restaurar Database Settings (via SQL)
|
|
8
|
+
*/
|
|
9
|
+
module.exports = async ({ backupPath, targetProject }) => {
|
|
10
|
+
console.log(chalk.blue('\n🔧 Restaurando Database Settings...'));
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const files = fs.readdirSync(backupPath);
|
|
14
|
+
const dbSettingsFile = files.find(f => f.startsWith('database-settings-') && f.endsWith('.json'));
|
|
15
|
+
|
|
16
|
+
if (!dbSettingsFile) {
|
|
17
|
+
console.log(chalk.yellow(' ⚠️ Nenhuma configuração de Database encontrada no backup'));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const dbSettingsData = JSON.parse(fs.readFileSync(path.join(backupPath, dbSettingsFile), 'utf8'));
|
|
22
|
+
const dbSettings = dbSettingsData.database_settings || dbSettingsData;
|
|
23
|
+
|
|
24
|
+
const extensions = dbSettings.extensions || [];
|
|
25
|
+
|
|
26
|
+
if (extensions.length > 0) {
|
|
27
|
+
console.log(chalk.gray(` - Habilitando ${extensions.length} extension(s)...`));
|
|
28
|
+
|
|
29
|
+
for (const ext of extensions) {
|
|
30
|
+
const extName = typeof ext === 'string' ? ext : ext.name;
|
|
31
|
+
console.log(chalk.gray(` - ${extName}`));
|
|
32
|
+
|
|
33
|
+
const sqlCommand = `CREATE EXTENSION IF NOT EXISTS ${extName};`;
|
|
34
|
+
|
|
35
|
+
const urlMatch = targetProject.targetDatabaseUrl.match(/postgresql:\/\/([^@:]+):([^@]+)@(.+)$/);
|
|
36
|
+
|
|
37
|
+
if (!urlMatch) {
|
|
38
|
+
console.log(chalk.yellow(` ⚠️ URL inválida para ${extName}`));
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const dockerCmd = [
|
|
43
|
+
'docker run --rm',
|
|
44
|
+
'--network host',
|
|
45
|
+
`-e PGPASSWORD="${encodeURIComponent(urlMatch[2])}"`,
|
|
46
|
+
'postgres:17 psql',
|
|
47
|
+
`-d "${targetProject.targetDatabaseUrl}"`,
|
|
48
|
+
`-c "${sqlCommand}"`
|
|
49
|
+
].join(' ');
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
execSync(dockerCmd, { stdio: 'pipe', encoding: 'utf8' });
|
|
53
|
+
} catch (sqlError) {
|
|
54
|
+
console.log(chalk.yellow(` ⚠️ ${extName} - extension já existe ou não pode ser habilitada`));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(chalk.green(' ✅ Database Settings restaurados com sucesso!'));
|
|
60
|
+
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(chalk.red(` ❌ Erro ao restaurar Database Settings: ${error.message}`));
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Etapa 8: Restaurar Realtime Settings (interativo - exibir URL e valores)
|
|
8
|
+
*/
|
|
9
|
+
module.exports = async ({ backupPath, targetProject }) => {
|
|
10
|
+
console.log(chalk.blue('\n🔄 Restaurando Realtime Settings...'));
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const realtimeSettingsPath = path.join(backupPath, 'realtime-settings.json');
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(realtimeSettingsPath)) {
|
|
16
|
+
console.log(chalk.yellow(' ⚠️ Nenhuma configuração de Realtime encontrada no backup'));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const realtimeSettings = JSON.parse(fs.readFileSync(realtimeSettingsPath, 'utf8'));
|
|
21
|
+
const dashboardUrl = `https://supabase.com/dashboard/project/${targetProject.targetProjectId}/realtime/settings`;
|
|
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 (realtimeSettings.realtime_settings?.settings) {
|
|
28
|
+
Object.entries(realtimeSettings.realtime_settings.settings).forEach(([key, setting]) => {
|
|
29
|
+
console.log(chalk.gray(` - ${setting.label}: ${setting.value}`));
|
|
30
|
+
if (setting.description) {
|
|
31
|
+
console.log(chalk.gray(` ${setting.description}`));
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(chalk.yellow('\n ⚠️ Após configurar, pressione Enter para continuar...'));
|
|
37
|
+
|
|
38
|
+
await inquirer.prompt([{
|
|
39
|
+
type: 'input',
|
|
40
|
+
name: 'continue',
|
|
41
|
+
message: 'Pressione Enter para continuar'
|
|
42
|
+
}]);
|
|
43
|
+
|
|
44
|
+
console.log(chalk.green(' ✅ Realtime Settings processados'));
|
|
45
|
+
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(chalk.red(` ❌ Erro ao processar Realtime Settings: ${error.message}`));
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Listar backups válidos (aceita .backup.gz e .backup)
|
|
7
|
+
*/
|
|
8
|
+
async function listValidBackups(backupsDir) {
|
|
9
|
+
if (!fs.existsSync(backupsDir)) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const items = fs.readdirSync(backupsDir, { withFileTypes: true });
|
|
14
|
+
const validBackups = [];
|
|
15
|
+
|
|
16
|
+
for (const item of items) {
|
|
17
|
+
if (item.isDirectory() && item.name.startsWith('backup-')) {
|
|
18
|
+
const backupPath = path.join(backupsDir, item.name);
|
|
19
|
+
const files = fs.readdirSync(backupPath);
|
|
20
|
+
// Aceitar tanto .backup.gz quanto .backup
|
|
21
|
+
const backupFile = files.find(file =>
|
|
22
|
+
file.endsWith('.backup.gz') || file.endsWith('.backup')
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (backupFile) {
|
|
26
|
+
const manifestPath = path.join(backupPath, 'backup-manifest.json');
|
|
27
|
+
let manifest = null;
|
|
28
|
+
|
|
29
|
+
if (fs.existsSync(manifestPath)) {
|
|
30
|
+
try {
|
|
31
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// Ignorar erro de leitura do manifest
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const stats = fs.statSync(path.join(backupPath, backupFile));
|
|
38
|
+
|
|
39
|
+
validBackups.push({
|
|
40
|
+
name: item.name,
|
|
41
|
+
path: backupPath,
|
|
42
|
+
backupFile: backupFile,
|
|
43
|
+
created: manifest?.created_at || stats.birthtime.toISOString(),
|
|
44
|
+
projectId: manifest?.project_id || 'Desconhecido',
|
|
45
|
+
size: formatBytes(stats.size),
|
|
46
|
+
manifest: manifest
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return validBackups.sort((a, b) => new Date(b.created) - new Date(a.created));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Formatar bytes
|
|
57
|
+
*/
|
|
58
|
+
function formatBytes(bytes) {
|
|
59
|
+
if (bytes === 0) return '0 Bytes';
|
|
60
|
+
const k = 1024;
|
|
61
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
62
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
63
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Mostrar resumo da restauração
|
|
68
|
+
*/
|
|
69
|
+
function showRestoreSummary(backup, components, targetProject) {
|
|
70
|
+
console.log(chalk.blue('\n📋 Resumo da Restauração:'));
|
|
71
|
+
console.log(chalk.blue('═'.repeat(80)));
|
|
72
|
+
console.log(chalk.cyan(`📦 Backup: ${backup.name}`));
|
|
73
|
+
console.log(chalk.cyan(`📤 Projeto Origem: ${backup.projectId}`));
|
|
74
|
+
console.log(chalk.cyan(`📥 Projeto Destino: ${targetProject.targetProjectId}`));
|
|
75
|
+
console.log('');
|
|
76
|
+
console.log(chalk.cyan('Componentes que serão restaurados:'));
|
|
77
|
+
console.log('');
|
|
78
|
+
|
|
79
|
+
if (components.database) {
|
|
80
|
+
console.log('✅ Database (psql -f via Docker)');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (components.edgeFunctions) {
|
|
84
|
+
const edgeFunctionsDir = path.join(backup.path, 'edge-functions');
|
|
85
|
+
const functions = fs.readdirSync(edgeFunctionsDir).filter(item =>
|
|
86
|
+
fs.statSync(path.join(edgeFunctionsDir, item)).isDirectory()
|
|
87
|
+
);
|
|
88
|
+
console.log(`⚡ Edge Functions: ${functions.length} function(s)`);
|
|
89
|
+
functions.forEach(func => console.log(` - ${func}`));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (components.authSettings) {
|
|
93
|
+
console.log('🔐 Auth Settings: Exibir URL e valores para configuração manual');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (components.storage) {
|
|
97
|
+
console.log('📦 Storage Buckets: Exibir informações e instruções do Google Colab');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (components.databaseSettings) {
|
|
101
|
+
console.log('🔧 Database Extensions and Settings: Restaurar via SQL');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (components.realtimeSettings) {
|
|
105
|
+
console.log('🔄 Realtime Settings: Exibir URL e valores para configuração manual');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log('');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Função auxiliar para copiar diretório recursivamente
|
|
113
|
+
*/
|
|
114
|
+
async function copyDirectoryRecursive(src, dest) {
|
|
115
|
+
const fs = require('fs').promises;
|
|
116
|
+
|
|
117
|
+
await fs.mkdir(dest, { recursive: true });
|
|
118
|
+
|
|
119
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
120
|
+
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
const srcPath = path.join(src, entry.name);
|
|
123
|
+
const destPath = path.join(dest, entry.name);
|
|
124
|
+
|
|
125
|
+
if (entry.isDirectory()) {
|
|
126
|
+
await copyDirectoryRecursive(srcPath, destPath);
|
|
127
|
+
} else {
|
|
128
|
+
await fs.copyFile(srcPath, destPath);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
listValidBackups,
|
|
135
|
+
formatBytes,
|
|
136
|
+
showRestoreSummary,
|
|
137
|
+
copyDirectoryRecursive
|
|
138
|
+
};
|
|
139
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Copia um diretório de forma segura (retorna 0 se src não existir)
|
|
6
|
+
* @param {string} src - Diretório origem
|
|
7
|
+
* @param {string} dest - Diretório destino
|
|
8
|
+
* @returns {Promise<number>} - Quantidade de arquivos copiados (0 se src não existir)
|
|
9
|
+
*/
|
|
10
|
+
async function copyDirSafe(src, dest) {
|
|
11
|
+
try {
|
|
12
|
+
await fs.access(src);
|
|
13
|
+
} catch {
|
|
14
|
+
return 0; // Diretório não existe, retornar 0
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const stats = await fs.stat(src);
|
|
18
|
+
if (!stats.isDirectory()) {
|
|
19
|
+
return 0; // Não é diretório
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Criar diretório destino
|
|
23
|
+
await fs.mkdir(dest, { recursive: true });
|
|
24
|
+
|
|
25
|
+
// Contar e copiar arquivos
|
|
26
|
+
let count = 0;
|
|
27
|
+
const entries = await fs.readdir(src);
|
|
28
|
+
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const srcPath = path.join(src, entry);
|
|
31
|
+
const destPath = path.join(dest, entry);
|
|
32
|
+
|
|
33
|
+
const entryStats = await fs.stat(srcPath);
|
|
34
|
+
if (entryStats.isDirectory()) {
|
|
35
|
+
count += await copyDirSafe(srcPath, destPath);
|
|
36
|
+
} else {
|
|
37
|
+
await fs.copyFile(srcPath, destPath);
|
|
38
|
+
count++;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return count;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Limpa um diretório completamente (rm -rf) e recria (mkdir -p)
|
|
47
|
+
* @param {string} dir - Diretório a limpar
|
|
48
|
+
* @returns {Promise<void>}
|
|
49
|
+
*/
|
|
50
|
+
async function cleanDir(dir) {
|
|
51
|
+
try {
|
|
52
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
53
|
+
} catch {
|
|
54
|
+
// Ignorar erro se não existir
|
|
55
|
+
}
|
|
56
|
+
await fs.mkdir(dir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Conta arquivos em um diretório recursivamente
|
|
61
|
+
* @param {string} dir - Diretório a contar
|
|
62
|
+
* @returns {Promise<number>} - Número de arquivos (0 se não existir)
|
|
63
|
+
*/
|
|
64
|
+
async function countFiles(dir) {
|
|
65
|
+
try {
|
|
66
|
+
await fs.access(dir);
|
|
67
|
+
} catch {
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const stats = await fs.stat(dir);
|
|
72
|
+
if (!stats.isDirectory()) {
|
|
73
|
+
return 1; // É um arquivo
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let count = 0;
|
|
77
|
+
const entries = await fs.readdir(dir);
|
|
78
|
+
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
const entryPath = path.join(dir, entry);
|
|
81
|
+
const entryStats = await fs.stat(entryPath);
|
|
82
|
+
|
|
83
|
+
if (entryStats.isDirectory()) {
|
|
84
|
+
count += await countFiles(entryPath);
|
|
85
|
+
} else {
|
|
86
|
+
count++;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return count;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
copyDirSafe,
|
|
95
|
+
cleanDir,
|
|
96
|
+
countFiles
|
|
97
|
+
};
|
|
98
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs').promises;
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extrai a senha da URL de conexão PostgreSQL
|
|
8
|
+
* @param {string} dbUrl - URL completa (postgresql://user:password@host:port/db)
|
|
9
|
+
* @returns {string} - Senha extraída
|
|
10
|
+
*/
|
|
11
|
+
function extractPasswordFromDbUrl(dbUrl) {
|
|
12
|
+
const urlMatch = dbUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
|
|
13
|
+
if (!urlMatch) {
|
|
14
|
+
throw new Error('Database URL inválida');
|
|
15
|
+
}
|
|
16
|
+
const [, , password] = urlMatch;
|
|
17
|
+
return password;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Garante que o projeto está corretamente linkado, removendo .temp e refazendo o link
|
|
22
|
+
* @param {string} projectRef - ID do projeto Supabase
|
|
23
|
+
* @param {string} accessToken - Token de acesso Supabase
|
|
24
|
+
* @param {string} dbPassword - Senha do banco de dados
|
|
25
|
+
* @returns {Promise<void>}
|
|
26
|
+
*/
|
|
27
|
+
async function ensureCleanLink(projectRef, accessToken, dbPassword) {
|
|
28
|
+
const tempDir = path.join(process.cwd(), 'supabase', '.temp');
|
|
29
|
+
|
|
30
|
+
// Remover supabase/.temp completamente
|
|
31
|
+
console.log(chalk.gray(` - Zerando vínculo e linkando projeto: ${projectRef}...`));
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
35
|
+
} catch (error) {
|
|
36
|
+
// Ignorar erro se não existir
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Executar supabase link com env local (não modificar process.env global)
|
|
40
|
+
const env = {
|
|
41
|
+
...process.env,
|
|
42
|
+
SUPABASE_ACCESS_TOKEN: accessToken
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
execSync(`supabase link --project-ref ${projectRef} --password ${dbPassword}`, {
|
|
47
|
+
stdio: 'pipe',
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
timeout: 15000,
|
|
50
|
+
env
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new Error(`Falha ao linkar projeto ${projectRef}: ${error.message}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Validar: ler supabase/.temp/project-ref e verificar se == projectRef
|
|
57
|
+
const projectRefFile = path.join(tempDir, 'project-ref');
|
|
58
|
+
try {
|
|
59
|
+
const linkedRef = await fs.readFile(projectRefFile, 'utf8');
|
|
60
|
+
const linkedRefTrimmed = linkedRef.trim();
|
|
61
|
+
|
|
62
|
+
if (linkedRefTrimmed !== projectRef) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Validação falhou: linked-ref = ${linkedRefTrimmed} (esperado = ${projectRef}). ` +
|
|
65
|
+
`O projeto linkado não corresponde ao projeto esperado.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(chalk.gray(` - Validação: linked-ref = ${linkedRefTrimmed} (esperado = ${projectRef})`));
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error.message.includes('Validação falhou')) {
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
throw new Error(`Não foi possível validar o vínculo: ${error.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
extractPasswordFromDbUrl,
|
|
80
|
+
ensureCleanLink
|
|
81
|
+
};
|
|
82
|
+
|