smoonb 0.0.23 → 0.0.25
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/bin/smoonb.js +1 -0
- package/package.json +1 -1
- package/src/commands/backup.js +13 -28
- package/src/utils/realtime-settings.js +205 -0
package/bin/smoonb.js
CHANGED
|
@@ -70,6 +70,7 @@ program
|
|
|
70
70
|
.command('backup')
|
|
71
71
|
.description('Fazer backup completo do projeto Supabase usando Supabase CLI')
|
|
72
72
|
.option('-o, --output <dir>', 'Diretório de saída do backup')
|
|
73
|
+
.option('--skip-realtime', 'Pular captura interativa de Realtime Settings')
|
|
73
74
|
.action(commands.backup);
|
|
74
75
|
|
|
75
76
|
program
|
package/package.json
CHANGED
package/src/commands/backup.js
CHANGED
|
@@ -8,6 +8,7 @@ const { sha256 } = require('../utils/hash');
|
|
|
8
8
|
const { readConfig, validateFor } = require('../utils/config');
|
|
9
9
|
const { showBetaBanner } = require('../utils/banner');
|
|
10
10
|
const { canPerformCompleteBackup, getDockerVersion } = require('../utils/docker');
|
|
11
|
+
const { captureRealtimeSettings } = require('../utils/realtime-settings');
|
|
11
12
|
|
|
12
13
|
const execAsync = promisify(exec);
|
|
13
14
|
|
|
@@ -135,9 +136,9 @@ async function performFullBackup(config, options) {
|
|
|
135
136
|
const rolesResult = await backupCustomRoles(config.supabase.databaseUrl, backupDir);
|
|
136
137
|
manifest.components.custom_roles = rolesResult;
|
|
137
138
|
|
|
138
|
-
// 6. Backup Realtime Settings via
|
|
139
|
-
console.log(chalk.blue('\n🔄 6/6 - Backup das Realtime Settings via
|
|
140
|
-
const realtimeResult = await backupRealtimeSettings(config.supabase.
|
|
139
|
+
// 6. Backup Realtime Settings via Captura Interativa
|
|
140
|
+
console.log(chalk.blue('\n🔄 6/6 - Backup das Realtime Settings via Captura Interativa...'));
|
|
141
|
+
const realtimeResult = await backupRealtimeSettings(config.supabase.projectId, backupDir, options.skipRealtime);
|
|
141
142
|
manifest.components.realtime = realtimeResult;
|
|
142
143
|
|
|
143
144
|
// Salvar manifest
|
|
@@ -150,7 +151,7 @@ async function performFullBackup(config, options) {
|
|
|
150
151
|
console.log(chalk.green(`🔐 Auth Settings: ${authResult.success ? 'Exportadas via API' : 'Falharam'}`));
|
|
151
152
|
console.log(chalk.green(`📦 Storage: ${storageResult.buckets?.length || 0} buckets verificados via API`));
|
|
152
153
|
console.log(chalk.green(`👥 Custom Roles: ${rolesResult.roles?.length || 0} roles exportados via SQL`));
|
|
153
|
-
console.log(chalk.green(`🔄 Realtime: ${realtimeResult.success ? 'Configurações
|
|
154
|
+
console.log(chalk.green(`🔄 Realtime: ${realtimeResult.success ? 'Configurações capturadas interativamente' : 'Falharam'}`));
|
|
154
155
|
|
|
155
156
|
return { success: true, backupDir, manifest };
|
|
156
157
|
}
|
|
@@ -551,37 +552,21 @@ async function backupCustomRoles(databaseUrl, backupDir) {
|
|
|
551
552
|
}
|
|
552
553
|
}
|
|
553
554
|
|
|
554
|
-
// Backup das Realtime Settings via
|
|
555
|
-
async function backupRealtimeSettings(
|
|
555
|
+
// Backup das Realtime Settings via Captura Interativa
|
|
556
|
+
async function backupRealtimeSettings(projectId, backupDir, skipInteractive = false) {
|
|
556
557
|
try {
|
|
557
|
-
console.log(chalk.gray(' -
|
|
558
|
+
console.log(chalk.gray(' - Capturando Realtime Settings interativamente...'));
|
|
558
559
|
|
|
559
|
-
const
|
|
560
|
+
const result = await captureRealtimeSettings(projectId, backupDir, skipInteractive);
|
|
560
561
|
|
|
561
|
-
|
|
562
|
-
// Nota: Supabase CLI não tem comando específico para Realtime
|
|
563
|
-
// Vamos criar um arquivo placeholder com informações sobre Realtime
|
|
564
|
-
|
|
565
|
-
const realtimeContent = {
|
|
566
|
-
project_id: databaseUrl.split('@')[1]?.split('.')[0] || 'unknown',
|
|
567
|
-
timestamp: new Date().toISOString(),
|
|
568
|
-
note: 'Realtime settings are managed via Supabase Dashboard',
|
|
569
|
-
message: 'Para configurar Realtime, acesse o Dashboard do Supabase',
|
|
570
|
-
url: 'https://supabase.com/dashboard/project/[PROJECT_ID]/settings/api',
|
|
571
|
-
documentation: 'https://supabase.com/docs/guides/realtime'
|
|
572
|
-
};
|
|
573
|
-
|
|
574
|
-
await writeJson(realtimeFile, realtimeContent);
|
|
575
|
-
|
|
576
|
-
const stats = fs.statSync(realtimeFile);
|
|
562
|
+
const stats = fs.statSync(path.join(backupDir, 'realtime-settings.json'));
|
|
577
563
|
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
578
564
|
|
|
579
|
-
console.log(chalk.green(` ✅ Realtime Settings
|
|
580
|
-
console.log(chalk.gray(` ℹ️ Realtime é gerenciado via Dashboard do Supabase`));
|
|
565
|
+
console.log(chalk.green(` ✅ Realtime Settings capturadas: ${sizeKB} KB`));
|
|
581
566
|
|
|
582
|
-
return { success: true };
|
|
567
|
+
return { success: true, settings: result };
|
|
583
568
|
} catch (error) {
|
|
584
|
-
console.log(chalk.yellow(` ⚠️ Erro ao
|
|
569
|
+
console.log(chalk.yellow(` ⚠️ Erro ao capturar Realtime Settings: ${error.message}`));
|
|
585
570
|
return { success: false };
|
|
586
571
|
}
|
|
587
572
|
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
const fs = require('fs').promises;
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Captura configurações de Realtime Settings interativamente
|
|
7
|
+
* @param {string} projectId - ID do projeto Supabase
|
|
8
|
+
* @param {string} backupDir - Diretório do backup atual
|
|
9
|
+
* @param {boolean} skipInteractive - Se deve pular a etapa interativa
|
|
10
|
+
* @returns {Promise<Object>} Configurações capturadas
|
|
11
|
+
*/
|
|
12
|
+
async function captureRealtimeSettings(projectId, backupDir, skipInteractive = false) {
|
|
13
|
+
const settingsFile = path.join(backupDir, 'realtime-settings.json');
|
|
14
|
+
const dashboardUrl = `https://supabase.com/dashboard/project/${projectId}/realtime/settings`;
|
|
15
|
+
|
|
16
|
+
// Tentar ler configurações de backup anterior
|
|
17
|
+
const previousSettings = await getPreviousRealtimeSettings(backupDir);
|
|
18
|
+
|
|
19
|
+
if (skipInteractive && previousSettings) {
|
|
20
|
+
console.log('📋 Usando configurações de Realtime Settings do backup anterior...');
|
|
21
|
+
await fs.writeFile(settingsFile, JSON.stringify(previousSettings, null, 2));
|
|
22
|
+
return previousSettings;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (previousSettings && !skipInteractive) {
|
|
26
|
+
const shouldReuse = await askToReusePreviousSettings();
|
|
27
|
+
if (shouldReuse) {
|
|
28
|
+
console.log('📋 Reutilizando configurações de Realtime Settings do backup anterior...');
|
|
29
|
+
await fs.writeFile(settingsFile, JSON.stringify(previousSettings, null, 2));
|
|
30
|
+
return previousSettings;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Capturar configurações interativamente
|
|
35
|
+
console.log('\n🔧 Configurações de Realtime Settings');
|
|
36
|
+
console.log('═'.repeat(50));
|
|
37
|
+
console.log(`📱 Acesse: ${dashboardUrl}`);
|
|
38
|
+
console.log('📝 Anote os valores dos 4 parâmetros abaixo:\n');
|
|
39
|
+
|
|
40
|
+
const settings = await captureSettingsInteractively(projectId, previousSettings);
|
|
41
|
+
|
|
42
|
+
// Salvar configurações
|
|
43
|
+
await fs.writeFile(settingsFile, JSON.stringify(settings, null, 2));
|
|
44
|
+
console.log('\n✅ Configurações de Realtime Settings salvas!');
|
|
45
|
+
|
|
46
|
+
return settings;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Busca configurações de backup anterior
|
|
51
|
+
* @param {string} backupDir - Diretório do backup atual
|
|
52
|
+
* @returns {Promise<Object|null>} Configurações anteriores ou null
|
|
53
|
+
*/
|
|
54
|
+
async function getPreviousRealtimeSettings(backupDir) {
|
|
55
|
+
try {
|
|
56
|
+
// Buscar em backups anteriores
|
|
57
|
+
const backupsDir = path.dirname(backupDir);
|
|
58
|
+
const entries = await fs.readdir(backupsDir, { withFileTypes: true });
|
|
59
|
+
const backupDirs = entries
|
|
60
|
+
.filter(entry => entry.isDirectory() && entry.name.startsWith('backup-'))
|
|
61
|
+
.map(entry => entry.name)
|
|
62
|
+
.sort()
|
|
63
|
+
.reverse(); // Mais recente primeiro
|
|
64
|
+
|
|
65
|
+
for (const backupName of backupDirs) {
|
|
66
|
+
const settingsPath = path.join(backupsDir, backupName, 'realtime-settings.json');
|
|
67
|
+
try {
|
|
68
|
+
const content = await fs.readFile(settingsPath, 'utf8');
|
|
69
|
+
const settings = JSON.parse(content);
|
|
70
|
+
if (settings.realtime_settings && settings.realtime_settings.settings) {
|
|
71
|
+
return settings;
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
// Continuar para próximo backup
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Pergunta se deve reutilizar configurações anteriores
|
|
87
|
+
* @returns {Promise<boolean>} true se deve reutilizar
|
|
88
|
+
*/
|
|
89
|
+
async function askToReusePreviousSettings() {
|
|
90
|
+
const rl = readline.createInterface({
|
|
91
|
+
input: process.stdin,
|
|
92
|
+
output: process.stdout
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
rl.question('🔄 Foi identificada uma gravação anterior de Realtime Settings.\n Deseja reutilizar as configurações anteriores? (S/n): ', (answer) => {
|
|
97
|
+
rl.close();
|
|
98
|
+
const shouldReuse = !answer.toLowerCase().startsWith('n');
|
|
99
|
+
resolve(shouldReuse);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Captura configurações interativamente via perguntas
|
|
106
|
+
* @param {string} projectId - ID do projeto Supabase
|
|
107
|
+
* @param {Object} previousSettings - Configurações anteriores para usar como padrão
|
|
108
|
+
* @returns {Promise<Object>} Configurações capturadas
|
|
109
|
+
*/
|
|
110
|
+
async function captureSettingsInteractively(projectId, previousSettings) {
|
|
111
|
+
const rl = readline.createInterface({
|
|
112
|
+
input: process.stdin,
|
|
113
|
+
output: process.stdout
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const askQuestion = (question, defaultValue) => {
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
rl.question(`${question} [${defaultValue}]: `, (answer) => {
|
|
119
|
+
resolve(answer.trim() || defaultValue);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
console.log('📋 Responda as perguntas abaixo (pressione Enter para usar o valor padrão):\n');
|
|
126
|
+
|
|
127
|
+
// Valores padrão baseados na imagem ou configurações anteriores
|
|
128
|
+
const defaults = previousSettings?.realtime_settings?.settings || {
|
|
129
|
+
allow_public_access: { value: true },
|
|
130
|
+
database_connection_pool_size: { value: 2 },
|
|
131
|
+
max_concurrent_clients: { value: 200 },
|
|
132
|
+
max_events_per_second: { value: 100 }
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const allowPublicAccess = await askQuestion(
|
|
136
|
+
'1. Allow public access (true/false):',
|
|
137
|
+
defaults.allow_public_access.value
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const poolSize = await askQuestion(
|
|
141
|
+
'2. Database connection pool size:',
|
|
142
|
+
defaults.database_connection_pool_size.value
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const maxClients = await askQuestion(
|
|
146
|
+
'3. Max concurrent clients:',
|
|
147
|
+
defaults.max_concurrent_clients.value
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const maxEvents = await askQuestion(
|
|
151
|
+
'4. Max events per second:',
|
|
152
|
+
defaults.max_events_per_second.value
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const settings = {
|
|
156
|
+
realtime_settings: {
|
|
157
|
+
note: "Configurações de Realtime Settings capturadas interativamente",
|
|
158
|
+
dashboard_url: `https://supabase.com/dashboard/project/${projectId}/realtime/settings`,
|
|
159
|
+
captured_at: new Date().toISOString(),
|
|
160
|
+
settings: {
|
|
161
|
+
allow_public_access: {
|
|
162
|
+
label: "Allow public access",
|
|
163
|
+
description: "If disabled, only private channels will be allowed",
|
|
164
|
+
value: allowPublicAccess === 'true' || allowPublicAccess === true
|
|
165
|
+
},
|
|
166
|
+
database_connection_pool_size: {
|
|
167
|
+
label: "Database connection pool size",
|
|
168
|
+
description: "Realtime Authorization uses this database pool to check client access",
|
|
169
|
+
value: parseInt(poolSize)
|
|
170
|
+
},
|
|
171
|
+
max_concurrent_clients: {
|
|
172
|
+
label: "Max concurrent clients",
|
|
173
|
+
description: "Sets maximum number of concurrent clients that can connect to your Realtime service",
|
|
174
|
+
value: parseInt(maxClients)
|
|
175
|
+
},
|
|
176
|
+
max_events_per_second: {
|
|
177
|
+
label: "Max events per second",
|
|
178
|
+
description: "Sets maximum number of events per second that can be sent to your Realtime service",
|
|
179
|
+
value: parseInt(maxEvents)
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
restore_instructions: {
|
|
183
|
+
url: `https://supabase.com/dashboard/project/${projectId}/realtime/settings`,
|
|
184
|
+
steps: [
|
|
185
|
+
"1. Acesse a URL acima",
|
|
186
|
+
"2. Configure 'Allow public access' conforme o valor em settings.allow_public_access.value",
|
|
187
|
+
"3. Configure 'Database connection pool size' conforme o valor em settings.database_connection_pool_size.value",
|
|
188
|
+
"4. Configure 'Max concurrent clients' conforme o valor em settings.max_concurrent_clients.value",
|
|
189
|
+
"5. Configure 'Max events per second' conforme o valor em settings.max_events_per_second.value",
|
|
190
|
+
"6. Clique em 'Save changes'"
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return settings;
|
|
197
|
+
|
|
198
|
+
} finally {
|
|
199
|
+
rl.close();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = {
|
|
204
|
+
captureRealtimeSettings
|
|
205
|
+
};
|