smoonb 1.0.7 → 1.0.9

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.
@@ -1,69 +1,87 @@
1
- const chalk = require('chalk');
2
- const path = require('path');
3
- const fs = require('fs');
4
- const { execSync } = require('child_process');
5
- const { t } = require('../../../i18n');
6
-
7
- /**
8
- * Etapa 7: Restaurar Database Settings (via SQL)
9
- */
10
- module.exports = async ({ backupPath, targetProject }) => {
11
- try {
12
- const getT = global.smoonbI18n?.t || t;
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(` ⚠️ ${getT('restore.steps.databaseSettings.notFound')}`));
18
- return { success: false };
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.white(` - ${getT('restore.steps.databaseSettings.enabling', { count: extensions.length })}`));
28
-
29
- for (const ext of extensions) {
30
- const extName = typeof ext === 'string' ? ext : ext.name;
31
- console.log(chalk.white(` ${getT('restore.steps.databaseSettings.extension', { 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(` ⚠️ ${getT('restore.steps.databaseSettings.invalidUrl', { 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 {
54
- console.log(chalk.yellow(` ⚠️ ${getT('restore.steps.databaseSettings.extensionExists', { extName })}`));
55
- }
56
- }
57
- }
58
-
59
- console.log(chalk.green(` ✅ ${getT('restore.steps.databaseSettings.success')}`));
60
-
61
- return { success: true, extensions_count: extensions.length };
62
-
63
- } catch (error) {
64
- const getT = global.smoonbI18n?.t || t;
65
- console.error(chalk.red(` ❌ ${getT('restore.steps.databaseSettings.error', { message: error.message })}`));
66
- throw error;
67
- }
68
- };
69
-
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { runSql, ident, literal } = require('../pg');
4
+ module.exports = async ({ backupPath, targetProject, postgresMajor = 17 }) => {
5
+ const file = fs
6
+ .readdirSync(backupPath)
7
+ .find((f) => f.startsWith('database-settings-') && f.endsWith('.json'));
8
+ if (!file) throw new Error('Database settings missing from backup');
9
+ const data = JSON.parse(fs.readFileSync(path.join(backupPath, file), 'utf8'));
10
+ const settings = data.database_settings || data;
11
+ const query = async (sql) =>
12
+ JSON.parse(
13
+ await runSql(targetProject.targetDatabaseUrl, sql, { postgresMajor }),
14
+ );
15
+ const actual = await query(
16
+ "SELECT json_build_object('settings',(SELECT json_object_agg(name,setting) FROM pg_settings),'roles',(SELECT json_object_agg(rolname,rolconfig) FROM pg_roles),'extensions',(SELECT json_object_agg(extname,extversion) FROM pg_extension));",
17
+ );
18
+ const commands = [],
19
+ pending = [];
20
+ for (const ext of settings.extensions || []) {
21
+ if (!actual.extensions[ext.name])
22
+ commands.push(
23
+ `CREATE EXTENSION IF NOT EXISTS ${ident(ext.name)} WITH SCHEMA ${ident(ext.schema)};`,
24
+ );
25
+ }
26
+ for (const item of settings.postgres_settings || []) {
27
+ if (actual.settings[item.name] === item.setting) continue;
28
+ if (!['user', 'superuser'].includes(item.context)) {
29
+ pending.push(item.name);
30
+ continue;
31
+ }
32
+ const value =
33
+ item.unit && Number(item.setting) >= 0
34
+ ? `${Number(item.setting) * (item.unit === '8kB' ? 8 : 1)}${item.unit === '8kB' ? 'kB' : item.unit}`
35
+ : item.setting;
36
+ commands.push(
37
+ `ALTER DATABASE ${ident(new URL(targetProject.targetDatabaseUrl).pathname.slice(1) || 'postgres')} SET ${ident(item.name)} TO ${literal(value)};`,
38
+ );
39
+ }
40
+ for (const role of [
41
+ ...(settings.role_configurations || []),
42
+ ...(settings.pgaudit_configurations || []),
43
+ ]) {
44
+ for (const config of role.config || []) {
45
+ if ((actual.roles[role.role] || []).includes(config)) continue;
46
+ const eq = config.indexOf('=');
47
+ commands.push(
48
+ `ALTER ROLE ${ident(role.role)} SET ${ident(config.slice(0, eq))} TO ${literal(config.slice(eq + 1))};`,
49
+ );
50
+ }
51
+ }
52
+ if (pending.length)
53
+ throw new Error(
54
+ `Database platform settings require compatible destination configuration: ${pending.join(', ')}`,
55
+ );
56
+ if (commands.length)
57
+ await runSql(
58
+ targetProject.targetDatabaseUrl,
59
+ 'BEGIN;\n' + commands.join('\n') + '\nCOMMIT;',
60
+ { postgresMajor },
61
+ );
62
+ const after = await query(
63
+ "SELECT json_build_object('settings',(SELECT json_object_agg(name,setting) FROM pg_settings),'roles',(SELECT json_object_agg(rolname,rolconfig) FROM pg_roles),'extensions',(SELECT json_object_agg(extname,extversion) FROM pg_extension));",
64
+ );
65
+ const differences = (settings.postgres_settings || [])
66
+ .filter((s) => after.settings[s.name] !== s.setting)
67
+ .map((s) => s.name);
68
+ for (const role of settings.role_configurations || [])
69
+ if (
70
+ (role.config || []).some(
71
+ (c) => !(after.roles[role.role] || []).includes(c),
72
+ )
73
+ )
74
+ differences.push(`role:${role.role}`);
75
+ for (const ext of settings.extensions || [])
76
+ if (!after.extensions[ext.name]) differences.push(`extension:${ext.name}`);
77
+ if (differences.length)
78
+ throw new Error(
79
+ `Database settings verification failed: ${differences.join(', ')}`,
80
+ );
81
+ return {
82
+ success: true,
83
+ verified: true,
84
+ extensions_count: (settings.extensions || []).length,
85
+ settings_count: (settings.postgres_settings || []).length,
86
+ };
87
+ };
@@ -1,53 +1,61 @@
1
- const chalk = require('chalk');
2
- const path = require('path');
3
- const fs = require('fs');
4
- const inquirer = require('inquirer');
5
- const { t } = require('../../../i18n');
6
-
7
- /**
8
- * Etapa 8: Restaurar Realtime Settings (interativo - exibir URL e valores)
9
- */
10
- module.exports = async ({ backupPath, targetProject }) => {
11
-
12
- try {
13
- const getT = global.smoonbI18n?.t || t;
14
- const realtimeSettingsPath = path.join(backupPath, 'realtime-settings.json');
15
-
16
- if (!fs.existsSync(realtimeSettingsPath)) {
17
- console.log(chalk.yellow(` ⚠️ ${getT('restore.steps.realtime.notFound')}`));
18
- return;
19
- }
20
-
21
- const realtimeSettings = JSON.parse(fs.readFileSync(realtimeSettingsPath, 'utf8'));
22
- const dashboardUrl = `https://supabase.com/dashboard/project/${targetProject.targetProjectId}/realtime/settings`;
23
-
24
- console.log(chalk.green(`\n ✅ ${getT('restore.steps.realtime.urlTitle')}`));
25
- console.log(chalk.cyan(` ${dashboardUrl}`));
26
- console.log(chalk.yellow(`\n 📋 ${getT('restore.steps.realtime.configureTitle')}`));
27
-
28
- if (realtimeSettings.realtime_settings?.settings) {
29
- Object.values(realtimeSettings.realtime_settings.settings).forEach((setting) => {
30
- console.log(chalk.white(` - ${setting.label}: ${setting.value}`));
31
- if (setting.description) {
32
- console.log(chalk.white(` ${setting.description}`));
33
- }
34
- });
35
- }
36
-
37
- console.log(chalk.yellow(`\n ⚠️ ${getT('restore.steps.realtime.pressEnter')}`));
38
-
39
- await inquirer.prompt([{
40
- type: 'input',
41
- name: 'continue',
42
- message: getT('restore.steps.realtime.pressEnter'),
43
- prefix: ''
44
- }]);
45
-
46
- console.log(chalk.green(` ✅ ${getT('restore.steps.realtime.success')}`));
47
-
48
- } catch (error) {
49
- const getT = global.smoonbI18n?.t || t;
50
- console.error(chalk.red(` ❌ ${getT('restore.steps.realtime.error', { message: error.message })}`));
51
- }
52
- };
53
-
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { applyConfig } = require('../management');
4
+ const allowed = [
5
+ 'private_only',
6
+ 'connection_pool',
7
+ 'postgres_changes_pool',
8
+ 'max_concurrent_users',
9
+ 'max_events_per_second',
10
+ 'max_bytes_per_second',
11
+ 'max_channels_per_client',
12
+ 'max_joins_per_second',
13
+ 'max_presence_events_per_second',
14
+ 'max_payload_size_in_kb',
15
+ 'suspend',
16
+ 'presence_enabled',
17
+ ];
18
+ module.exports = async ({ backupPath, targetProject }) => {
19
+ const backup = JSON.parse(
20
+ fs.readFileSync(path.join(backupPath, 'realtime-settings.json'), 'utf8'),
21
+ );
22
+ let desired = backup.settings;
23
+ if (!desired) {
24
+ const legacy = backup.realtime_settings?.settings;
25
+ if (!legacy) throw new Error('Realtime configuration missing from backup');
26
+ const mapping = {
27
+ enable_realtime_service: 'suspend',
28
+ allow_public_access: 'private_only',
29
+ database_connection_pool_size: 'connection_pool',
30
+ max_concurrent_clients: 'max_concurrent_users',
31
+ max_events_per_second: 'max_events_per_second',
32
+ max_presence_events_per_second: 'max_presence_events_per_second',
33
+ max_payload_size_kb: 'max_payload_size_in_kb',
34
+ };
35
+ desired = Object.fromEntries(
36
+ Object.entries(legacy).map(([key, item]) => {
37
+ if (!mapping[key])
38
+ throw new Error(`Unsupported Realtime configuration: ${key}`);
39
+ return [
40
+ mapping[key],
41
+ ['enable_realtime_service', 'allow_public_access'].includes(key)
42
+ ? !item.value
43
+ : item.value,
44
+ ];
45
+ }),
46
+ );
47
+ console.log(
48
+ `Realtime: restoring the recorded snapshot (${backup.realtime_settings.captured_at || 'date unavailable'}).`,
49
+ );
50
+ }
51
+ const result = await applyConfig(
52
+ targetProject,
53
+ 'config/realtime',
54
+ desired,
55
+ allowed,
56
+ );
57
+ return {
58
+ ...result,
59
+ captured_at: backup.captured_at || backup.realtime_settings?.captured_at,
60
+ };
61
+ };