smoonb 0.0.26 → 0.0.28
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.js +228 -49
- package/src/utils/realtime-settings.js +1 -1
package/package.json
CHANGED
package/src/commands/backup.js
CHANGED
|
@@ -108,38 +108,37 @@ async function performFullBackup(config, options) {
|
|
|
108
108
|
};
|
|
109
109
|
|
|
110
110
|
// 1. Backup Database via pg_dumpall Docker (idêntico ao Dashboard)
|
|
111
|
-
console.log(chalk.blue('\n📊 1/
|
|
112
|
-
const
|
|
113
|
-
manifest.components.database =
|
|
114
|
-
success: dbResult.success,
|
|
115
|
-
method: 'pg_dumpall_docker',
|
|
116
|
-
fileName: dbResult.fileName,
|
|
117
|
-
size_kb: dbResult.size,
|
|
118
|
-
dashboard_compatible: true
|
|
119
|
-
};
|
|
111
|
+
console.log(chalk.blue('\n📊 1/7 - Backup da Database PostgreSQL via pg_dumpall Docker...'));
|
|
112
|
+
const databaseResult = await backupDatabase(config.supabase.projectId, backupDir);
|
|
113
|
+
manifest.components.database = databaseResult;
|
|
120
114
|
|
|
121
115
|
// 2. Backup Edge Functions via Docker
|
|
122
|
-
console.log(chalk.blue('\n⚡ 2/
|
|
116
|
+
console.log(chalk.blue('\n⚡ 2/7 - Backup das Edge Functions via Docker...'));
|
|
123
117
|
const functionsResult = await backupEdgeFunctionsWithDocker(config.supabase.projectId, config.supabase.accessToken, backupDir);
|
|
124
118
|
manifest.components.edge_functions = functionsResult;
|
|
125
119
|
|
|
126
120
|
// 3. Backup Auth Settings via API
|
|
127
|
-
console.log(chalk.blue('\n🔐 3/
|
|
121
|
+
console.log(chalk.blue('\n🔐 3/7 - Backup das Auth Settings via API...'));
|
|
128
122
|
const authResult = await backupAuthSettings(config.supabase.projectId, config.supabase.accessToken, backupDir);
|
|
129
123
|
manifest.components.auth_settings = authResult;
|
|
130
124
|
|
|
131
125
|
// 4. Backup Storage via API
|
|
132
|
-
console.log(chalk.blue('\n📦 4/
|
|
126
|
+
console.log(chalk.blue('\n📦 4/7 - Backup do Storage via API...'));
|
|
133
127
|
const storageResult = await backupStorage(config.supabase.projectId, config.supabase.accessToken, backupDir);
|
|
134
128
|
manifest.components.storage = storageResult;
|
|
135
129
|
|
|
136
130
|
// 5. Backup Custom Roles via SQL
|
|
137
|
-
console.log(chalk.blue('\n👥 5/
|
|
131
|
+
console.log(chalk.blue('\n👥 5/7 - Backup dos Custom Roles via SQL...'));
|
|
138
132
|
const rolesResult = await backupCustomRoles(config.supabase.databaseUrl, backupDir);
|
|
139
133
|
manifest.components.custom_roles = rolesResult;
|
|
140
134
|
|
|
141
|
-
// 6. Backup
|
|
142
|
-
console.log(chalk.blue('\n
|
|
135
|
+
// 6. Backup das Database Extensions and Settings via SQL
|
|
136
|
+
console.log(chalk.blue('\n🔧 6/7 - Backup das Database Extensions and Settings via SQL...'));
|
|
137
|
+
const databaseSettingsResult = await backupDatabaseSettings(config.supabase.projectId, backupDir);
|
|
138
|
+
manifest.components.database_settings = databaseSettingsResult;
|
|
139
|
+
|
|
140
|
+
// 7. Backup Realtime Settings via Captura Interativa
|
|
141
|
+
console.log(chalk.blue('\n🔄 7/7 - Backup das Realtime Settings via Captura Interativa...'));
|
|
143
142
|
const realtimeResult = await backupRealtimeSettings(config.supabase.projectId, backupDir, options.skipRealtime);
|
|
144
143
|
manifest.components.realtime = realtimeResult;
|
|
145
144
|
|
|
@@ -148,12 +147,22 @@ async function performFullBackup(config, options) {
|
|
|
148
147
|
|
|
149
148
|
console.log(chalk.green('\n🎉 BACKUP COMPLETO FINALIZADO VIA DOCKER!'));
|
|
150
149
|
console.log(chalk.blue(`📁 Localização: ${backupDir}`));
|
|
151
|
-
console.log(chalk.green(`📊 Database: ${
|
|
150
|
+
console.log(chalk.green(`📊 Database: ${databaseResult.fileName} (${databaseResult.size} KB) - Idêntico ao Dashboard`));
|
|
151
|
+
console.log(chalk.green(`🔧 Database Settings: ${databaseSettingsResult.fileName} (${databaseSettingsResult.size} KB) - Extensions e Configurações`));
|
|
152
152
|
console.log(chalk.green(`⚡ Edge Functions: ${functionsResult.success_count || 0}/${functionsResult.functions_count || 0} functions baixadas via Docker`));
|
|
153
153
|
console.log(chalk.green(`🔐 Auth Settings: ${authResult.success ? 'Exportadas via API' : 'Falharam'}`));
|
|
154
154
|
console.log(chalk.green(`📦 Storage: ${storageResult.buckets?.length || 0} buckets verificados via API`));
|
|
155
155
|
console.log(chalk.green(`👥 Custom Roles: ${rolesResult.roles?.length || 0} roles exportados via SQL`));
|
|
156
|
-
|
|
156
|
+
// Determinar mensagem correta baseada no método usado
|
|
157
|
+
let realtimeMessage = 'Falharam';
|
|
158
|
+
if (realtimeResult.success) {
|
|
159
|
+
if (options.skipRealtime) {
|
|
160
|
+
realtimeMessage = 'Configurações copiadas do backup anterior';
|
|
161
|
+
} else {
|
|
162
|
+
realtimeMessage = 'Configurações capturadas interativamente';
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
console.log(chalk.green(`🔄 Realtime: ${realtimeMessage}`));
|
|
157
166
|
|
|
158
167
|
return { success: true, backupDir, manifest };
|
|
159
168
|
}
|
|
@@ -220,12 +229,16 @@ function showDockerMessagesAndExit(reason) {
|
|
|
220
229
|
}
|
|
221
230
|
|
|
222
231
|
// Backup da database usando pg_dumpall via Docker (idêntico ao Supabase Dashboard)
|
|
223
|
-
async function
|
|
232
|
+
async function backupDatabase(projectId, backupDir) {
|
|
224
233
|
try {
|
|
225
|
-
console.log(chalk.gray('
|
|
234
|
+
console.log(chalk.gray(' - Criando backup completo via pg_dumpall...'));
|
|
235
|
+
|
|
236
|
+
const { execSync } = require('child_process');
|
|
237
|
+
const config = await readConfig();
|
|
226
238
|
|
|
227
239
|
// Extrair credenciais da databaseUrl
|
|
228
|
-
const
|
|
240
|
+
const dbUrl = config.supabase.databaseUrl;
|
|
241
|
+
const urlMatch = dbUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
|
|
229
242
|
|
|
230
243
|
if (!urlMatch) {
|
|
231
244
|
throw new Error('Database URL inválida');
|
|
@@ -233,7 +246,7 @@ async function backupDatabaseWithDocker(databaseUrl, backupDir) {
|
|
|
233
246
|
|
|
234
247
|
const [, username, password, host, port, database] = urlMatch;
|
|
235
248
|
|
|
236
|
-
// Gerar nome do arquivo igual ao dashboard
|
|
249
|
+
// Gerar nome do arquivo igual ao dashboard
|
|
237
250
|
const now = new Date();
|
|
238
251
|
const day = String(now.getDate()).padStart(2, '0');
|
|
239
252
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
@@ -243,14 +256,14 @@ async function backupDatabaseWithDocker(databaseUrl, backupDir) {
|
|
|
243
256
|
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
244
257
|
|
|
245
258
|
const fileName = `db_cluster-${day}-${month}-${year}@${hours}-${minutes}-${seconds}.backup`;
|
|
246
|
-
const filePath = path.join(backupDir, fileName);
|
|
247
259
|
|
|
248
|
-
|
|
260
|
+
// CORREÇÃO: Usar caminho absoluto igual às Edge Functions
|
|
261
|
+
const backupDirAbs = path.resolve(backupDir);
|
|
249
262
|
|
|
250
|
-
// Comando pg_dumpall via Docker (
|
|
263
|
+
// Comando pg_dumpall via Docker (mesma abordagem das Edge Functions)
|
|
251
264
|
const dockerCmd = [
|
|
252
265
|
'docker run --rm --network host',
|
|
253
|
-
`-v "${
|
|
266
|
+
`-v "${backupDirAbs}:/host"`,
|
|
254
267
|
`-e PGPASSWORD="${password}"`,
|
|
255
268
|
'postgres:17 pg_dumpall',
|
|
256
269
|
`-h ${host}`,
|
|
@@ -259,43 +272,28 @@ async function backupDatabaseWithDocker(databaseUrl, backupDir) {
|
|
|
259
272
|
`-f /host/${fileName}`
|
|
260
273
|
].join(' ');
|
|
261
274
|
|
|
262
|
-
console.log(chalk.gray(
|
|
263
|
-
|
|
275
|
+
console.log(chalk.gray(` - Executando pg_dumpall via Docker...`));
|
|
276
|
+
execSync(dockerCmd, { stdio: 'pipe' });
|
|
264
277
|
|
|
265
278
|
// Compactar igual ao Supabase Dashboard
|
|
266
|
-
console.log(chalk.gray(' - Compactando arquivo...'));
|
|
267
279
|
const gzipCmd = [
|
|
268
280
|
'docker run --rm',
|
|
269
|
-
`-v "${
|
|
270
|
-
|
|
281
|
+
`-v "${backupDirAbs}:/host"`,
|
|
282
|
+
`postgres:17 gzip /host/${fileName}`
|
|
271
283
|
].join(' ');
|
|
272
284
|
|
|
273
|
-
|
|
285
|
+
execSync(gzipCmd, { stdio: 'pipe' });
|
|
274
286
|
|
|
275
287
|
const finalFileName = `${fileName}.gz`;
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
// Validar arquivo gerado
|
|
279
|
-
if (!fs.existsSync(finalFilePath)) {
|
|
280
|
-
throw new Error('Arquivo de backup não foi criado');
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const stats = fs.statSync(finalFilePath);
|
|
288
|
+
const stats = fs.statSync(path.join(backupDir, finalFileName));
|
|
284
289
|
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
285
290
|
|
|
286
291
|
console.log(chalk.green(` ✅ Database backup: ${finalFileName} (${sizeKB} KB)`));
|
|
287
292
|
|
|
288
|
-
return {
|
|
289
|
-
success: true,
|
|
290
|
-
fileName: finalFileName,
|
|
291
|
-
size: sizeKB,
|
|
292
|
-
method: 'pg_dumpall_docker',
|
|
293
|
-
dashboard_compatible: true
|
|
294
|
-
};
|
|
295
|
-
|
|
293
|
+
return { success: true, size: sizeKB, fileName: finalFileName };
|
|
296
294
|
} catch (error) {
|
|
297
|
-
console.log(chalk.
|
|
298
|
-
return { success: false
|
|
295
|
+
console.log(chalk.yellow(` ⚠️ Erro no backup do database: ${error.message}`));
|
|
296
|
+
return { success: false };
|
|
299
297
|
}
|
|
300
298
|
}
|
|
301
299
|
|
|
@@ -541,6 +539,187 @@ async function backupCustomRoles(databaseUrl, backupDir) {
|
|
|
541
539
|
}
|
|
542
540
|
}
|
|
543
541
|
|
|
542
|
+
// Backup das Database Extensions and Settings via SQL
|
|
543
|
+
async function backupDatabaseSettings(projectId, backupDir) {
|
|
544
|
+
try {
|
|
545
|
+
console.log(chalk.gray(' - Capturando Database Extensions and Settings...'));
|
|
546
|
+
|
|
547
|
+
const { execSync } = require('child_process');
|
|
548
|
+
const config = await readConfig();
|
|
549
|
+
|
|
550
|
+
// Extrair credenciais da databaseUrl
|
|
551
|
+
const dbUrl = config.supabase.databaseUrl;
|
|
552
|
+
const urlMatch = dbUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
|
|
553
|
+
|
|
554
|
+
if (!urlMatch) {
|
|
555
|
+
throw new Error('Database URL inválida');
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const [, username, password, host, port, database] = urlMatch;
|
|
559
|
+
|
|
560
|
+
// Gerar nome do arquivo
|
|
561
|
+
const now = new Date();
|
|
562
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
563
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
564
|
+
const year = now.getFullYear();
|
|
565
|
+
const hours = String(now.getHours()).padStart(2, '0');
|
|
566
|
+
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
567
|
+
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
568
|
+
|
|
569
|
+
const fileName = `database-settings-${day}-${month}-${year}@${hours}-${minutes}-${seconds}.json`;
|
|
570
|
+
|
|
571
|
+
// Usar caminho absoluto igual às outras funções
|
|
572
|
+
const backupDirAbs = path.resolve(backupDir);
|
|
573
|
+
|
|
574
|
+
// Script SQL para capturar todas as configurações
|
|
575
|
+
const sqlScript = `
|
|
576
|
+
-- Database Extensions and Settings Backup
|
|
577
|
+
-- Generated at: ${new Date().toISOString()}
|
|
578
|
+
|
|
579
|
+
-- 1. Capturar extensões instaladas
|
|
580
|
+
SELECT json_agg(
|
|
581
|
+
json_build_object(
|
|
582
|
+
'name', extname,
|
|
583
|
+
'version', extversion,
|
|
584
|
+
'schema', extnamespace::regnamespace
|
|
585
|
+
)
|
|
586
|
+
) as extensions
|
|
587
|
+
FROM pg_extension
|
|
588
|
+
ORDER BY extname;
|
|
589
|
+
|
|
590
|
+
-- 2. Capturar configurações PostgreSQL importantes
|
|
591
|
+
SELECT json_agg(
|
|
592
|
+
json_build_object(
|
|
593
|
+
'name', name,
|
|
594
|
+
'setting', setting,
|
|
595
|
+
'unit', unit,
|
|
596
|
+
'context', context,
|
|
597
|
+
'description', short_desc
|
|
598
|
+
)
|
|
599
|
+
) as postgres_settings
|
|
600
|
+
FROM pg_settings
|
|
601
|
+
WHERE name IN (
|
|
602
|
+
'statement_timeout',
|
|
603
|
+
'idle_in_transaction_session_timeout',
|
|
604
|
+
'lock_timeout',
|
|
605
|
+
'shared_buffers',
|
|
606
|
+
'work_mem',
|
|
607
|
+
'maintenance_work_mem',
|
|
608
|
+
'effective_cache_size',
|
|
609
|
+
'max_connections',
|
|
610
|
+
'log_statement',
|
|
611
|
+
'log_min_duration_statement',
|
|
612
|
+
'timezone',
|
|
613
|
+
'log_timezone',
|
|
614
|
+
'default_transaction_isolation',
|
|
615
|
+
'default_transaction_read_only',
|
|
616
|
+
'checkpoint_completion_target',
|
|
617
|
+
'wal_buffers',
|
|
618
|
+
'max_wal_size',
|
|
619
|
+
'min_wal_size'
|
|
620
|
+
)
|
|
621
|
+
ORDER BY name;
|
|
622
|
+
|
|
623
|
+
-- 3. Capturar configurações específicas dos roles Supabase
|
|
624
|
+
SELECT json_agg(
|
|
625
|
+
json_build_object(
|
|
626
|
+
'role', rolname,
|
|
627
|
+
'config', rolconfig
|
|
628
|
+
)
|
|
629
|
+
) as role_configurations
|
|
630
|
+
FROM pg_roles
|
|
631
|
+
WHERE rolname IN ('anon', 'authenticated', 'authenticator', 'postgres', 'service_role')
|
|
632
|
+
AND rolconfig IS NOT NULL
|
|
633
|
+
ORDER BY rolname;
|
|
634
|
+
|
|
635
|
+
-- 4. Capturar configurações de PGAudit (se existir)
|
|
636
|
+
SELECT json_agg(
|
|
637
|
+
json_build_object(
|
|
638
|
+
'role', rolname,
|
|
639
|
+
'config', rolconfig
|
|
640
|
+
)
|
|
641
|
+
) as pgaudit_configurations
|
|
642
|
+
FROM pg_roles
|
|
643
|
+
WHERE rolconfig IS NOT NULL
|
|
644
|
+
AND EXISTS (
|
|
645
|
+
SELECT 1 FROM unnest(rolconfig) AS config
|
|
646
|
+
WHERE config LIKE '%pgaudit%'
|
|
647
|
+
)
|
|
648
|
+
ORDER BY rolname;
|
|
649
|
+
`;
|
|
650
|
+
|
|
651
|
+
// Salvar script SQL temporário
|
|
652
|
+
const sqlFile = path.join(backupDir, 'temp_settings.sql');
|
|
653
|
+
await fs.writeFile(sqlFile, sqlScript);
|
|
654
|
+
|
|
655
|
+
// Executar via Docker
|
|
656
|
+
const dockerCmd = [
|
|
657
|
+
'docker run --rm --network host',
|
|
658
|
+
`-v "${backupDirAbs}:/host"`,
|
|
659
|
+
`-e PGPASSWORD="${password}"`,
|
|
660
|
+
'postgres:17 psql',
|
|
661
|
+
`-h ${host}`,
|
|
662
|
+
`-p ${port}`,
|
|
663
|
+
`-U ${username}`,
|
|
664
|
+
`-d ${database}`,
|
|
665
|
+
'-f /host/temp_settings.sql',
|
|
666
|
+
'-t', // Tuples only
|
|
667
|
+
'-A', // Unaligned output
|
|
668
|
+
'-F', // Field separator
|
|
669
|
+
'|'
|
|
670
|
+
].join(' ');
|
|
671
|
+
|
|
672
|
+
console.log(chalk.gray(' - Executando queries de configurações via Docker...'));
|
|
673
|
+
const output = execSync(dockerCmd, { stdio: 'pipe', encoding: 'utf8' });
|
|
674
|
+
|
|
675
|
+
// Processar output e criar JSON estruturado
|
|
676
|
+
const lines = output.trim().split('\n').filter(line => line.trim());
|
|
677
|
+
|
|
678
|
+
const result = {
|
|
679
|
+
database_settings: {
|
|
680
|
+
note: "Configurações específicas do database Supabase capturadas via SQL",
|
|
681
|
+
captured_at: new Date().toISOString(),
|
|
682
|
+
project_id: projectId,
|
|
683
|
+
extensions: lines[0] ? JSON.parse(lines[0]) : [],
|
|
684
|
+
postgres_settings: lines[1] ? JSON.parse(lines[1]) : [],
|
|
685
|
+
role_configurations: lines[2] ? JSON.parse(lines[2]) : [],
|
|
686
|
+
pgaudit_configurations: lines[3] ? JSON.parse(lines[3]) : [],
|
|
687
|
+
restore_instructions: {
|
|
688
|
+
note: "Estas configurações precisam ser aplicadas manualmente após a restauração do database",
|
|
689
|
+
steps: [
|
|
690
|
+
"1. Restaurar o database usando o arquivo .backup.gz",
|
|
691
|
+
"2. Aplicar configurações de Postgres via SQL:",
|
|
692
|
+
" ALTER DATABASE postgres SET setting_name TO 'value';",
|
|
693
|
+
"3. Aplicar configurações de roles via SQL:",
|
|
694
|
+
" ALTER ROLE role_name SET setting_name TO 'value';",
|
|
695
|
+
"4. Habilitar extensões necessárias via Dashboard ou SQL:",
|
|
696
|
+
" CREATE EXTENSION IF NOT EXISTS extension_name;",
|
|
697
|
+
"5. Verificar configurações aplicadas:",
|
|
698
|
+
" SELECT name, setting FROM pg_settings WHERE name IN (...);"
|
|
699
|
+
]
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
// Salvar arquivo JSON
|
|
705
|
+
const jsonFile = path.join(backupDir, fileName);
|
|
706
|
+
await fs.writeFile(jsonFile, JSON.stringify(result, null, 2));
|
|
707
|
+
|
|
708
|
+
// Limpar arquivo temporário
|
|
709
|
+
await fs.unlink(sqlFile);
|
|
710
|
+
|
|
711
|
+
const stats = fs.statSync(jsonFile);
|
|
712
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
713
|
+
|
|
714
|
+
console.log(chalk.green(` ✅ Database Settings: ${fileName} (${sizeKB} KB)`));
|
|
715
|
+
|
|
716
|
+
return { success: true, size: sizeKB, fileName: fileName };
|
|
717
|
+
} catch (error) {
|
|
718
|
+
console.log(chalk.yellow(` ⚠️ Erro no backup das Database Settings: ${error.message}`));
|
|
719
|
+
return { success: false };
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
544
723
|
// Backup das Realtime Settings via Captura Interativa
|
|
545
724
|
async function backupRealtimeSettings(projectId, backupDir, skipInteractive = false) {
|
|
546
725
|
try {
|
|
@@ -17,7 +17,7 @@ async function captureRealtimeSettings(projectId, backupDir, skipInteractive = f
|
|
|
17
17
|
const previousSettings = await getPreviousRealtimeSettings(backupDir);
|
|
18
18
|
|
|
19
19
|
if (skipInteractive && previousSettings) {
|
|
20
|
-
console.log('📋
|
|
20
|
+
console.log('📋 Copiando Realtime Settings do backup anterior...');
|
|
21
21
|
await fs.writeFile(settingsFile, JSON.stringify(previousSettings, null, 2));
|
|
22
22
|
return previousSettings;
|
|
23
23
|
}
|