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/package.json CHANGED
@@ -1,66 +1,67 @@
1
- {
2
- "name": "smoonb",
3
- "version": "1.0.6",
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": "echo \"Error: no test specified\" && exit 1",
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": "node scripts/build.js"
21
- },
22
- "keywords": [
23
- "supabase",
24
- "backup",
25
- "migration",
26
- "postgresql",
27
- "edge-functions",
28
- "database",
29
- "cli",
30
- "tool"
31
- ],
32
- "author": "Goalmoon Tecnologia LTDA <https://www.smoonb.com>",
33
- "license": "SEE LICENSE IN LICENSE.md",
34
- "engines": {
35
- "node": ">=16.0.0"
36
- },
37
- "dependencies": {
38
- "@supabase/supabase-js": "^2.93.0",
39
- "adm-zip": "^0.5.16",
40
- "chalk": "^4.1.2",
41
- "commander": "^11.1.0",
42
- "env-paths": "^4.0.0",
43
- "inquirer": "^8.2.7"
44
- },
45
- "type": "commonjs",
46
- "devDependencies": {
47
- "eslint": "^9.38.0"
48
- },
49
- "repository": {
50
- "type": "git",
51
- "url": "git+https://github.com/almmello/smoonb.git"
52
- },
53
- "bugs": {
54
- "url": "https://github.com/almmello/smoonb/issues"
55
- },
56
- "homepage": "https://www.smoonb.com",
57
- "files": [
58
- "bin/",
59
- "src/",
60
- "README.md",
61
- "README.pt-BR.md",
62
- "LICENSE",
63
- "LICENSE.pt-BR.md",
64
- "CHANGELOG.md"
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
- console.log(chalk.green(`📊 ${getT('backup.complete.databaseSql', { count: dbSeparatedResult.files?.length || 0, size: `${dbSeparatedResult.totalSizeKB} KB` })}`));
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
- function runWithElapsedTicker(command, args, env, label) {
8
- return new Promise((resolve, reject) => {
9
- const start = Date.now();
10
- const ticker = setInterval(() => {
11
- const elapsed = Math.floor((Date.now() - start) / 1000);
12
- process.stdout.write(`\r ⏱ ${label} ${elapsed}s`);
13
- }, 1000);
14
- const proc = spawn(command, args, {
15
- stdio: ['ignore', 'pipe', 'pipe'],
16
- shell: true,
17
- env: { ...process.env, ...env }
18
- });
19
- proc.stderr.on('data', (chunk) => process.stderr.write(chunk));
20
- proc.on('close', (code) => {
21
- clearInterval(ticker);
22
- process.stdout.write('\r' + ' '.repeat(60) + '\r');
23
- if (code !== 0) reject(new Error(`Exited with code ${code}`));
24
- else resolve();
25
- });
26
- proc.on('error', reject);
27
- });
28
- }
29
-
30
- /**
31
- * Etapa 2: Backup Database Separado (SQL files para troubleshooting)
32
- */
33
- module.exports = async ({ databaseUrl, backupDir, accessToken }) => {
34
- try {
35
- const getT = global.smoonbI18n?.t || t;
36
- console.log(chalk.white(` - ${getT('backup.steps.database.separated.creating')}`));
37
-
38
- const dbUrl = databaseUrl;
39
- const files = [];
40
- let totalSizeKB = 0;
41
-
42
- // 1. Backup do Schema
43
- console.log(chalk.white(` - ${getT('backup.steps.database.separated.exportingSchema')}`));
44
- const schemaFile = path.join(backupDir, 'schema.sql');
45
-
46
- try {
47
- await runWithElapsedTicker(
48
- `supabase db dump --db-url "${dbUrl}" -f "${schemaFile}"`,
49
- [],
50
- { SUPABASE_ACCESS_TOKEN: accessToken || '' },
51
- getT('backup.steps.database.separated.exportingSchema')
52
- );
53
- const stats = await fs.stat(schemaFile);
54
- const sizeKB = (stats.size / 1024).toFixed(1);
55
- files.push({ filename: 'schema.sql', sizeKB });
56
- totalSizeKB += parseFloat(sizeKB);
57
- console.log(chalk.green(` ✅ Schema: ${sizeKB} KB`));
58
- } catch (error) {
59
- console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.database.separated.schemaError', { message: error.message })}`));
60
- }
61
-
62
- // 2. Backup dos Dados
63
- console.log(chalk.white(` - ${getT('backup.steps.database.separated.exportingData')}`));
64
- const dataFile = path.join(backupDir, 'data.sql');
65
-
66
- try {
67
- await runWithElapsedTicker(
68
- `supabase db dump --db-url "${dbUrl}" --data-only -f "${dataFile}"`,
69
- [],
70
- { SUPABASE_ACCESS_TOKEN: accessToken || '' },
71
- getT('backup.steps.database.separated.exportingData')
72
- );
73
- const stats = await fs.stat(dataFile);
74
- const sizeKB = (stats.size / 1024).toFixed(1);
75
- files.push({ filename: 'data.sql', sizeKB });
76
- totalSizeKB += parseFloat(sizeKB);
77
- console.log(chalk.green(` ✅ Data: ${sizeKB} KB`));
78
- } catch (error) {
79
- console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.database.separated.dataError', { message: error.message })}`));
80
- }
81
-
82
- // 3. Backup dos Roles
83
- console.log(chalk.white(` - ${getT('backup.steps.database.separated.exportingRoles')}`));
84
- const rolesFile = path.join(backupDir, 'roles.sql');
85
-
86
- try {
87
- await runWithElapsedTicker(
88
- `supabase db dump --db-url "${dbUrl}" --role-only -f "${rolesFile}"`,
89
- [],
90
- { SUPABASE_ACCESS_TOKEN: accessToken || '' },
91
- getT('backup.steps.database.separated.exportingRoles')
92
- );
93
- const stats = await fs.stat(rolesFile);
94
- const sizeKB = (stats.size / 1024).toFixed(1);
95
- files.push({ filename: 'roles.sql', sizeKB });
96
- totalSizeKB += parseFloat(sizeKB);
97
- console.log(chalk.green(` ✅ Roles: ${sizeKB} KB`));
98
- } catch (error) {
99
- console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.database.separated.rolesError', { message: error.message })}`));
100
- }
101
-
102
- return {
103
- success: files.length > 0,
104
- files,
105
- totalSizeKB: totalSizeKB.toFixed(1)
106
- };
107
-
108
- } catch (error) {
109
- const getT = global.smoonbI18n?.t || t;
110
- console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.database.separated.error', { message: error.message })}`));
111
- return { success: false, files: [], totalSizeKB: '0.0' };
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;
@@ -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 };