smoonb 1.0.6 → 1.0.7
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/CHANGELOG.md +18 -0
- package/README.md +663 -644
- package/README.pt-BR.md +663 -644
- package/package.json +67 -66
- package/src/commands/backup/index.js +8 -2
- package/src/commands/backup/steps/03-database-separated.js +161 -114
- package/src/i18n/locales/en.json +4 -0
- package/src/i18n/locales/pt-BR.json +4 -0
- package/src/utils/pg-dump-diagnostics.js +64 -0
package/package.json
CHANGED
|
@@ -1,66 +1,67 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "smoonb",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Complete Supabase backup and migration tool. https://www.smoonb.com/#price",
|
|
5
|
-
"preferGlobal": false,
|
|
6
|
-
"preventGlobalInstall": true,
|
|
7
|
-
"main": "index.js",
|
|
8
|
-
"bin": {
|
|
9
|
-
"smoonb": "bin/smoonb.js"
|
|
10
|
-
},
|
|
11
|
-
"scripts": {
|
|
12
|
-
"test": "
|
|
13
|
-
"start": "node bin/smoonb.js",
|
|
14
|
-
"preinstall": "node -e \"if(process.env.npm_config_global) { console.error('\\n❌ SMOONB NÃO DEVE SER INSTALADO GLOBALMENTE!\\n\\n📋 Para usar o smoonb, instale localmente no seu projeto:\\n npm install smoonb\\n\\n💡 Depois execute com:\\n npx smoonb backup\\n\\n🚫 Instalação global cancelada!\\n'); process.exit(1); }\"",
|
|
15
|
-
"postinstall": "echo '\\n✅ smoonb instalado com sucesso!\\n💡 Execute: npx smoonb backup\\n📖 Documentação: https://github.com/almmello/smoonb\\n'",
|
|
16
|
-
"lint": "eslint . --ext .js",
|
|
17
|
-
"lint:fix": "eslint . --ext .js --fix",
|
|
18
|
-
"build": "node scripts/build.js",
|
|
19
|
-
"build:check": "node scripts/build.js",
|
|
20
|
-
"prepublishOnly": "
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"README.
|
|
62
|
-
"
|
|
63
|
-
"LICENSE
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "smoonb",
|
|
3
|
+
"version": "1.0.7",
|
|
4
|
+
"description": "Complete Supabase backup and migration tool. https://www.smoonb.com/#price",
|
|
5
|
+
"preferGlobal": false,
|
|
6
|
+
"preventGlobalInstall": true,
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"smoonb": "bin/smoonb.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "node --test tests/*.test.js",
|
|
13
|
+
"start": "node bin/smoonb.js",
|
|
14
|
+
"preinstall": "node -e \"if(process.env.npm_config_global) { console.error('\\n❌ SMOONB NÃO DEVE SER INSTALADO GLOBALMENTE!\\n\\n📋 Para usar o smoonb, instale localmente no seu projeto:\\n npm install smoonb\\n\\n💡 Depois execute com:\\n npx smoonb backup\\n\\n🚫 Instalação global cancelada!\\n'); process.exit(1); }\"",
|
|
15
|
+
"postinstall": "echo '\\n✅ smoonb instalado com sucesso!\\n💡 Execute: npx smoonb backup\\n📖 Documentação: https://github.com/almmello/smoonb\\n'",
|
|
16
|
+
"lint": "eslint . --ext .js",
|
|
17
|
+
"lint:fix": "eslint . --ext .js --fix",
|
|
18
|
+
"build": "node scripts/build.js",
|
|
19
|
+
"build:check": "node scripts/build.js",
|
|
20
|
+
"prepublishOnly": "npm test && npm run lint && npm run build",
|
|
21
|
+
"test:docker": "node tests/integration/circular-restore.js"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"supabase",
|
|
25
|
+
"backup",
|
|
26
|
+
"migration",
|
|
27
|
+
"postgresql",
|
|
28
|
+
"edge-functions",
|
|
29
|
+
"database",
|
|
30
|
+
"cli",
|
|
31
|
+
"tool"
|
|
32
|
+
],
|
|
33
|
+
"author": "Goalmoon Tecnologia LTDA <https://www.smoonb.com>",
|
|
34
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=16.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@supabase/supabase-js": "^2.93.0",
|
|
40
|
+
"adm-zip": "^0.5.16",
|
|
41
|
+
"chalk": "^4.1.2",
|
|
42
|
+
"commander": "^11.1.0",
|
|
43
|
+
"env-paths": "^4.0.0",
|
|
44
|
+
"inquirer": "^8.2.7"
|
|
45
|
+
},
|
|
46
|
+
"type": "commonjs",
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"eslint": "^9.38.0"
|
|
49
|
+
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "git+https://github.com/almmello/smoonb.git"
|
|
53
|
+
},
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/almmello/smoonb/issues"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://www.smoonb.com",
|
|
58
|
+
"files": [
|
|
59
|
+
"bin/",
|
|
60
|
+
"src/",
|
|
61
|
+
"README.md",
|
|
62
|
+
"README.pt-BR.md",
|
|
63
|
+
"LICENSE",
|
|
64
|
+
"LICENSE.pt-BR.md",
|
|
65
|
+
"CHANGELOG.md"
|
|
66
|
+
]
|
|
67
|
+
}
|
|
@@ -267,7 +267,9 @@ module.exports = async (options) => {
|
|
|
267
267
|
const dbSeparatedResult = await step03DatabaseSeparated(context);
|
|
268
268
|
manifest.components.database_separated = {
|
|
269
269
|
success: dbSeparatedResult.success,
|
|
270
|
-
method: 'supabase-cli',
|
|
270
|
+
method: 'supabase-cli',
|
|
271
|
+
warnings: dbSeparatedResult.warnings || [],
|
|
272
|
+
failed_files: dbSeparatedResult.failedFiles || [],
|
|
271
273
|
files: dbSeparatedResult.files || [],
|
|
272
274
|
total_size_kb: dbSeparatedResult.totalSizeKB || '0.0'
|
|
273
275
|
};
|
|
@@ -339,7 +341,11 @@ module.exports = async (options) => {
|
|
|
339
341
|
console.log(chalk.green(`\n🎉 ${getT('backup.complete.title')}`));
|
|
340
342
|
console.log(chalk.blue(`📁 ${getT('backup.complete.location', { path: finalBackupDir })}`));
|
|
341
343
|
console.log(chalk.green(`📊 ${getT('backup.complete.database', { fileName: databaseResult.fileName, size: `${databaseResult.size} KB` })}`));
|
|
342
|
-
|
|
344
|
+
if (dbSeparatedResult.success) {
|
|
345
|
+
console.log(chalk.green(`📊 ${getT('backup.complete.databaseSql', { count: dbSeparatedResult.files?.length || 0, size: `${dbSeparatedResult.totalSizeKB} KB` })}`));
|
|
346
|
+
} else {
|
|
347
|
+
console.log(chalk.yellow(`⚠️ ${getT('backup.steps.database.separated.incomplete', { files: (dbSeparatedResult.failedFiles || []).join(', ') })}`));
|
|
348
|
+
}
|
|
343
349
|
console.log(chalk.green(`🔧 ${getT('backup.complete.databaseSettings', { fileName: databaseSettingsResult.fileName, size: `${databaseSettingsResult.size} KB` })}`));
|
|
344
350
|
|
|
345
351
|
if (flags?.includeAuth && manifest.components.auth_settings) {
|
|
@@ -1,114 +1,161 @@
|
|
|
1
|
-
const chalk = require('chalk');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const fs = require('fs').promises;
|
|
4
|
-
const { spawn } = require('child_process');
|
|
5
|
-
const { t } = require('../../../i18n');
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
);
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs').promises;
|
|
4
|
+
const { spawn } = require('child_process');
|
|
5
|
+
const { t } = require('../../../i18n');
|
|
6
|
+
const { createPgDumpDiagnostics, hasReplicaRolePrologue } = require('../../../utils/pg-dump-diagnostics');
|
|
7
|
+
|
|
8
|
+
function runWithElapsedTicker(command, args, env, label, collectWarnings = false) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const start = Date.now();
|
|
11
|
+
const useTty = Boolean(process.stdout.isTTY);
|
|
12
|
+
const clearTicker = () => {
|
|
13
|
+
if (useTty) process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
14
|
+
};
|
|
15
|
+
const diagnostics = createPgDumpDiagnostics(chunk => {
|
|
16
|
+
clearTicker();
|
|
17
|
+
process.stderr.write(chunk);
|
|
18
|
+
});
|
|
19
|
+
const ticker = useTty ? setInterval(() => {
|
|
20
|
+
const elapsed = Math.floor((Date.now() - start) / 1000);
|
|
21
|
+
process.stdout.write(`\r ⏱ ${label} ${elapsed}s`);
|
|
22
|
+
}, 1000) : null;
|
|
23
|
+
let settled = false;
|
|
24
|
+
const finish = (error) => {
|
|
25
|
+
if (settled) return;
|
|
26
|
+
settled = true;
|
|
27
|
+
clearInterval(ticker);
|
|
28
|
+
clearTicker();
|
|
29
|
+
const result = diagnostics.end();
|
|
30
|
+
if (error) {
|
|
31
|
+
// A failed export must never hide the diagnostics already collected.
|
|
32
|
+
if (result.messages.length) process.stderr.write(result.messages.join('\n') + '\n');
|
|
33
|
+
reject(error);
|
|
34
|
+
} else resolve(result);
|
|
35
|
+
};
|
|
36
|
+
try {
|
|
37
|
+
const proc = spawn(command, args, {
|
|
38
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
39
|
+
shell: true,
|
|
40
|
+
env: { ...process.env, ...env }
|
|
41
|
+
});
|
|
42
|
+
// Drain stdout as well, so a verbose CLI cannot fill its pipe and hang.
|
|
43
|
+
proc.stdout.on('data', chunk => { clearTicker(); process.stdout.write(chunk); });
|
|
44
|
+
proc.stderr.on('data', chunk => {
|
|
45
|
+
if (collectWarnings) diagnostics.write(chunk);
|
|
46
|
+
else { clearTicker(); process.stderr.write(chunk); }
|
|
47
|
+
});
|
|
48
|
+
proc.on('close', code => finish(code === 0 ? null : new Error(`Exited with code ${code}`)));
|
|
49
|
+
proc.on('error', finish);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
finish(error);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Etapa 2: Backup Database Separado (SQL files para troubleshooting)
|
|
58
|
+
*/
|
|
59
|
+
function createDatabaseSeparatedStep(runDump = runWithElapsedTicker) {
|
|
60
|
+
return async ({ databaseUrl, backupDir, accessToken }) => {
|
|
61
|
+
try {
|
|
62
|
+
const getT = global.smoonbI18n?.t || t;
|
|
63
|
+
console.log(chalk.white(` - ${getT('backup.steps.database.separated.creating')}`));
|
|
64
|
+
|
|
65
|
+
const dbUrl = databaseUrl;
|
|
66
|
+
const files = [];
|
|
67
|
+
let totalSizeKB = 0;
|
|
68
|
+
const warnings = [];
|
|
69
|
+
const failedFiles = [];
|
|
70
|
+
|
|
71
|
+
// 1. Backup do Schema
|
|
72
|
+
console.log(chalk.white(` - ${getT('backup.steps.database.separated.exportingSchema')}`));
|
|
73
|
+
const schemaFile = path.join(backupDir, 'schema.sql');
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
await runDump(
|
|
77
|
+
`supabase db dump --db-url "${dbUrl}" -f "${schemaFile}"`,
|
|
78
|
+
[],
|
|
79
|
+
{ SUPABASE_ACCESS_TOKEN: accessToken || '' },
|
|
80
|
+
getT('backup.steps.database.separated.exportingSchema')
|
|
81
|
+
);
|
|
82
|
+
const stats = await fs.stat(schemaFile);
|
|
83
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
84
|
+
files.push({ filename: 'schema.sql', sizeKB });
|
|
85
|
+
totalSizeKB += parseFloat(sizeKB);
|
|
86
|
+
console.log(chalk.green(` ✅ Schema: ${sizeKB} KB`));
|
|
87
|
+
} catch (error) {
|
|
88
|
+
failedFiles.push('schema.sql');
|
|
89
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.database.separated.schemaError', { message: error.message })}`));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 2. Backup dos Dados
|
|
93
|
+
console.log(chalk.white(` - ${getT('backup.steps.database.separated.exportingData')}`));
|
|
94
|
+
const dataFile = path.join(backupDir, 'data.sql');
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const diagnostics = await runDump(
|
|
98
|
+
`supabase db dump --db-url "${dbUrl}" --data-only -f "${dataFile}"`,
|
|
99
|
+
[],
|
|
100
|
+
{ SUPABASE_ACCESS_TOKEN: accessToken || '' },
|
|
101
|
+
getT('backup.steps.database.separated.exportingData'),
|
|
102
|
+
true
|
|
103
|
+
);
|
|
104
|
+
const stats = await fs.stat(dataFile);
|
|
105
|
+
if (diagnostics.circularForeignKeys) {
|
|
106
|
+
const replicaRolePrologue = await hasReplicaRolePrologue(dataFile);
|
|
107
|
+
warnings.push({ code: 'CIRCULAR_FOREIGN_KEYS', ...diagnostics, replicaRolePrologue });
|
|
108
|
+
console.log(chalk.cyan(` ℹ️ ${getT('backup.steps.database.separated.circularSummary', { tables: diagnostics.tables.join(', ') })}`));
|
|
109
|
+
console.log(chalk.yellow(` ⚠️ ${getT(replicaRolePrologue
|
|
110
|
+
? 'backup.steps.database.separated.circularProtected'
|
|
111
|
+
: 'backup.steps.database.separated.circularUnprotected')}`));
|
|
112
|
+
}
|
|
113
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
114
|
+
files.push({ filename: 'data.sql', sizeKB });
|
|
115
|
+
totalSizeKB += parseFloat(sizeKB);
|
|
116
|
+
console.log(chalk.green(` ✅ Data: ${sizeKB} KB`));
|
|
117
|
+
} catch (error) {
|
|
118
|
+
failedFiles.push('data.sql');
|
|
119
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.database.separated.dataError', { message: error.message })}`));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 3. Backup dos Roles
|
|
123
|
+
console.log(chalk.white(` - ${getT('backup.steps.database.separated.exportingRoles')}`));
|
|
124
|
+
const rolesFile = path.join(backupDir, 'roles.sql');
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
await runDump(
|
|
128
|
+
`supabase db dump --db-url "${dbUrl}" --role-only -f "${rolesFile}"`,
|
|
129
|
+
[],
|
|
130
|
+
{ SUPABASE_ACCESS_TOKEN: accessToken || '' },
|
|
131
|
+
getT('backup.steps.database.separated.exportingRoles')
|
|
132
|
+
);
|
|
133
|
+
const stats = await fs.stat(rolesFile);
|
|
134
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
135
|
+
files.push({ filename: 'roles.sql', sizeKB });
|
|
136
|
+
totalSizeKB += parseFloat(sizeKB);
|
|
137
|
+
console.log(chalk.green(` ✅ Roles: ${sizeKB} KB`));
|
|
138
|
+
} catch (error) {
|
|
139
|
+
failedFiles.push('roles.sql');
|
|
140
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.database.separated.rolesError', { message: error.message })}`));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
success: files.length === 3,
|
|
145
|
+
warnings,
|
|
146
|
+
failedFiles,
|
|
147
|
+
files,
|
|
148
|
+
totalSizeKB: totalSizeKB.toFixed(1)
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const getT = global.smoonbI18n?.t || t;
|
|
153
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.database.separated.error', { message: error.message })}`));
|
|
154
|
+
return { success: false, files: [], warnings: [], failedFiles: ['schema.sql', 'data.sql', 'roles.sql'], totalSizeKB: '0.0' };
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = createDatabaseSeparatedStep();
|
|
160
|
+
module.exports.createDatabaseSeparatedStep = createDatabaseSeparatedStep;
|
|
161
|
+
module.exports.runWithElapsedTicker = runWithElapsedTicker;
|
package/src/i18n/locales/en.json
CHANGED
|
@@ -411,6 +411,10 @@
|
|
|
411
411
|
"backup.steps.database.separated.creating": "Creating separate SQL backups via Supabase CLI...",
|
|
412
412
|
"backup.steps.database.separated.exportingSchema": "Exporting schema...",
|
|
413
413
|
"backup.steps.database.separated.exportingData": "Exporting data...",
|
|
414
|
+
"backup.steps.database.separated.circularSummary": "Data export completed with circular foreign-key warnings: {tables}. These warnings do not mean the export failed.",
|
|
415
|
+
"backup.steps.database.separated.circularProtected": "data.sql starts with SET session_replication_role = replica. Restore schema before data, using a role allowed to change this setting and ON_ERROR_STOP=1. Test restoration before relying on this backup.",
|
|
416
|
+
"backup.steps.database.separated.circularUnprotected": "No replica-role prologue was found in data.sql. Circular references may prevent restoration. Follow the Supabase restore procedure and test in an isolated database.",
|
|
417
|
+
"backup.steps.database.separated.incomplete": "Separate SQL backup is incomplete. Failed files: {files}.",
|
|
414
418
|
"backup.steps.database.separated.exportingRoles": "Exporting roles...",
|
|
415
419
|
|
|
416
420
|
"backup.steps.databaseSettings.title": "Database Extensions and Settings Backup via SQL...",
|
|
@@ -411,6 +411,10 @@
|
|
|
411
411
|
"backup.steps.database.separated.creating": "Criando backups SQL separados via Supabase CLI...",
|
|
412
412
|
"backup.steps.database.separated.exportingSchema": "Exportando schema...",
|
|
413
413
|
"backup.steps.database.separated.exportingData": "Exportando dados...",
|
|
414
|
+
"backup.steps.database.separated.circularSummary": "Exportação dos dados concluída com avisos de chaves estrangeiras circulares: {tables}. Esses avisos não significam falha na exportação.",
|
|
415
|
+
"backup.steps.database.separated.circularProtected": "data.sql começa com SET session_replication_role = replica. Restaure o schema antes dos dados, com uma role autorizada a alterar essa configuração e ON_ERROR_STOP=1. Teste a restauração antes de depender deste backup.",
|
|
416
|
+
"backup.steps.database.separated.circularUnprotected": "O início de data.sql não contém a configuração replica. As referências circulares podem impedir a restauração. Siga o procedimento do Supabase e teste em banco isolado.",
|
|
417
|
+
"backup.steps.database.separated.incomplete": "Backup SQL separado incompleto. Arquivos que falharam: {files}.",
|
|
414
418
|
"backup.steps.database.separated.exportingRoles": "Exportando roles...",
|
|
415
419
|
|
|
416
420
|
"backup.steps.databaseSettings.title": "Backup das Database Extensions and Settings via SQL...",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const { StringDecoder } = require('string_decoder');
|
|
2
|
+
const fs = require('fs').promises;
|
|
3
|
+
|
|
4
|
+
// Only the exact, known pg_dump circular-FK block is consolidated. All other
|
|
5
|
+
// stderr (including errors and unfamiliar/localized warnings) remains visible.
|
|
6
|
+
function createPgDumpDiagnostics(writeStderr) {
|
|
7
|
+
const decoder = new StringDecoder('utf8');
|
|
8
|
+
let pending = '';
|
|
9
|
+
let inCircularWarning = false;
|
|
10
|
+
const warnings = [];
|
|
11
|
+
const tables = new Set();
|
|
12
|
+
const hints = new Set([
|
|
13
|
+
'pg_dump: hint: You might not be able to restore the dump without using --disable-triggers or temporarily dropping the constraints.',
|
|
14
|
+
'pg_dump: hint: Consider using a full dump instead of a --data-only dump to avoid this problem.'
|
|
15
|
+
]);
|
|
16
|
+
function line(raw) {
|
|
17
|
+
const text = raw.replace(/\r?\n$/, '');
|
|
18
|
+
if (/^pg_dump: warning: there are circular foreign-key constraints (?:among these tables|on this table):$/.test(text)) {
|
|
19
|
+
inCircularWarning = true;
|
|
20
|
+
warnings.push(text);
|
|
21
|
+
} else if (inCircularWarning && text.startsWith('pg_dump: detail: ')) {
|
|
22
|
+
tables.add(text.slice('pg_dump: detail: '.length));
|
|
23
|
+
warnings.push(text);
|
|
24
|
+
} else if (inCircularWarning && hints.has(text)) {
|
|
25
|
+
warnings.push(text);
|
|
26
|
+
} else {
|
|
27
|
+
inCircularWarning = false;
|
|
28
|
+
writeStderr(raw);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function consume(text) {
|
|
32
|
+
pending += text;
|
|
33
|
+
let end;
|
|
34
|
+
while ((end = pending.indexOf('\n')) !== -1) {
|
|
35
|
+
line(pending.slice(0, end + 1));
|
|
36
|
+
pending = pending.slice(end + 1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
write(chunk) { consume(decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))); },
|
|
41
|
+
end() {
|
|
42
|
+
consume(decoder.end());
|
|
43
|
+
if (pending) line(pending);
|
|
44
|
+
pending = '';
|
|
45
|
+
return { circularForeignKeys: warnings.length > 0, tables: [...tables], messages: warnings };
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Supabase prepends this command before pg_dump output. Check the first line,
|
|
51
|
+
// not arbitrary table contents: a data row containing SQL is not protection.
|
|
52
|
+
async function hasReplicaRolePrologue(file) {
|
|
53
|
+
const handle = await fs.open(file, 'r');
|
|
54
|
+
try {
|
|
55
|
+
const buffer = Buffer.alloc(256);
|
|
56
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
57
|
+
const firstLine = buffer.toString('utf8', 0, bytesRead).split('\n')[0].replace(/^\uFEFF/, '').trim();
|
|
58
|
+
return /^SET\s+session_replication_role\s*=\s*(?:replica|'replica');$/i.test(firstLine);
|
|
59
|
+
} finally {
|
|
60
|
+
await handle.close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { createPgDumpDiagnostics, hasReplicaRolePrologue };
|