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.
- package/CHANGELOG.md +43 -0
- package/README.md +27 -7
- package/README.pt-BR.md +27 -7
- package/package.json +4 -3
- package/src/commands/backup/steps/05-auth-settings.js +6 -2
- package/src/commands/backup/steps/06-realtime-settings.js +19 -33
- package/src/commands/backup/steps/07-storage.js +7 -12
- package/src/commands/backup/steps/09-edge-functions.js +76 -202
- package/src/commands/restore/auth-fields.json +237 -0
- package/src/commands/restore/auth.js +65 -0
- package/src/commands/restore/dump.js +284 -0
- package/src/commands/restore/index.js +8 -127
- package/src/commands/restore/management.js +96 -0
- package/src/commands/restore/pg.js +83 -0
- package/src/commands/restore/runner.js +140 -0
- package/src/commands/restore/steps/03-database.js +18 -84
- package/src/commands/restore/steps/04-edge-functions.js +95 -122
- package/src/commands/restore/steps/05-auth-settings.js +15 -54
- package/src/commands/restore/steps/06-storage.js +203 -584
- package/src/commands/restore/steps/07-database-settings.js +87 -69
- package/src/commands/restore/steps/08-realtime-settings.js +61 -53
- package/src/i18n/locales/en.json +803 -797
- package/src/i18n/locales/pt-BR.json +802 -796
- package/src/utils/auth-secrets.js +23 -0
- package/src/utils/supabaseCli.js +32 -0
|
@@ -12,17 +12,11 @@ const { isPostgresMajorSupported } = require('../backup/utils');
|
|
|
12
12
|
const step00License = require('../backup/steps/00-license');
|
|
13
13
|
const step01DockerValidation = require('../backup/steps/01-docker-validation');
|
|
14
14
|
|
|
15
|
-
const
|
|
15
|
+
const { runRestore } = require('./runner');
|
|
16
16
|
|
|
17
17
|
// Importar todas as etapas
|
|
18
18
|
const step00BackupSelection = require('./steps/00-backup-selection');
|
|
19
19
|
const step01ComponentsSelection = require('./steps/01-components-selection');
|
|
20
|
-
const step03Database = require('./steps/03-database');
|
|
21
|
-
const step04EdgeFunctions = require('./steps/04-edge-functions');
|
|
22
|
-
const step05AuthSettings = require('./steps/05-auth-settings');
|
|
23
|
-
const step06Storage = require('./steps/06-storage');
|
|
24
|
-
const step07DatabaseSettings = require('./steps/07-database-settings');
|
|
25
|
-
const step08RealtimeSettings = require('./steps/08-realtime-settings');
|
|
26
20
|
const { sendTelemetry } = require('../../telemetry');
|
|
27
21
|
|
|
28
22
|
module.exports = async (options = {}) => {
|
|
@@ -176,125 +170,9 @@ module.exports = async (options = {}) => {
|
|
|
176
170
|
process.exit(0);
|
|
177
171
|
}
|
|
178
172
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
ui.info(` ${getT('restore.disclaimer.errorsReason')}`);
|
|
183
|
-
ui.info('');
|
|
184
|
-
ui.info(` ${getT('restore.disclaimer.supabaseDoc')}`);
|
|
185
|
-
ui.link(` ${SUPABASE_RESTORE_DOC_URL}`);
|
|
186
|
-
ui.info('');
|
|
187
|
-
ui.warn(` ${getT('restore.disclaimer.waitComplete')}`);
|
|
188
|
-
ui.warn(` ${getT('restore.disclaimer.testResult')}`);
|
|
189
|
-
ui.info('');
|
|
190
|
-
const acceptErrors = await confirm(getT('restore.disclaimer.acceptProceed'), true);
|
|
191
|
-
if (!acceptErrors) {
|
|
192
|
-
ui.warn(`🚫 ${getT('restore.index.restoreCancelled')}`);
|
|
193
|
-
process.exit(0);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// 6. Executar restauração
|
|
197
|
-
ui.step(`\n🚀 ${getT('restore.index.startingRestore')}`);
|
|
198
|
-
|
|
199
|
-
// Contar etapas totais para numeração dinâmica
|
|
200
|
-
let stepNumber = 0;
|
|
201
|
-
const totalSteps = (components.database ? 1 : 0) +
|
|
202
|
-
(components.edgeFunctions ? 1 : 0) +
|
|
203
|
-
(components.authSettings ? 1 : 0) +
|
|
204
|
-
(components.storage ? 1 : 0) +
|
|
205
|
-
(components.databaseSettings ? 1 : 0) +
|
|
206
|
-
(components.realtimeSettings ? 1 : 0);
|
|
207
|
-
|
|
208
|
-
// Armazenar resultados para o resumo final
|
|
209
|
-
const restoreResults = {};
|
|
210
|
-
|
|
211
|
-
// 6.1 Database (se selecionado)
|
|
212
|
-
if (components.database) {
|
|
213
|
-
stepNumber++;
|
|
214
|
-
ui.step(`\n📊 ${stepNumber}/${totalSteps} - ${getT('restore.index.restoringDatabase')}`);
|
|
215
|
-
await step03Database({
|
|
216
|
-
backupFilePath: path.join(selectedBackup.path, selectedBackup.backupFile),
|
|
217
|
-
targetDatabaseUrl: targetProject.targetDatabaseUrl
|
|
218
|
-
});
|
|
219
|
-
restoreResults.database = { success: true };
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// 6.2 Edge Functions (se selecionado)
|
|
223
|
-
if (components.edgeFunctions) {
|
|
224
|
-
stepNumber++;
|
|
225
|
-
ui.step(`\n⚡ ${stepNumber}/${totalSteps} - ${getT('restore.index.restoringEdgeFunctions')}`);
|
|
226
|
-
const edgeFunctionsResult = await step04EdgeFunctions({
|
|
227
|
-
backupPath: selectedBackup.path,
|
|
228
|
-
targetProject
|
|
229
|
-
});
|
|
230
|
-
restoreResults.edgeFunctions = edgeFunctionsResult || { success: true };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// 6.3 Auth Settings (se selecionado)
|
|
234
|
-
if (components.authSettings) {
|
|
235
|
-
stepNumber++;
|
|
236
|
-
ui.step(`\n🔐 ${stepNumber}/${totalSteps} - ${getT('restore.index.restoringAuthSettings')}`);
|
|
237
|
-
await step05AuthSettings({
|
|
238
|
-
backupPath: selectedBackup.path,
|
|
239
|
-
targetProject
|
|
240
|
-
});
|
|
241
|
-
restoreResults.authSettings = { success: true };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// 6.4 Storage Buckets (se selecionado)
|
|
245
|
-
if (components.storage) {
|
|
246
|
-
stepNumber++;
|
|
247
|
-
ui.step(`\n📦 ${stepNumber}/${totalSteps} - ${getT('restore.index.restoringStorageBuckets')}`);
|
|
248
|
-
const storageResult = await step06Storage({
|
|
249
|
-
backupPath: selectedBackup.path,
|
|
250
|
-
targetProject
|
|
251
|
-
});
|
|
252
|
-
restoreResults.storage = storageResult || { success: true };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// 6.5 Database Settings (se selecionado)
|
|
256
|
-
if (components.databaseSettings) {
|
|
257
|
-
stepNumber++;
|
|
258
|
-
ui.step(`\n🔧 ${stepNumber}/${totalSteps} - ${getT('restore.index.restoringDatabaseSettings')}`);
|
|
259
|
-
await step07DatabaseSettings({
|
|
260
|
-
backupPath: selectedBackup.path,
|
|
261
|
-
targetProject
|
|
262
|
-
});
|
|
263
|
-
restoreResults.databaseSettings = { success: true };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// 6.6 Realtime Settings (se selecionado)
|
|
267
|
-
if (components.realtimeSettings) {
|
|
268
|
-
stepNumber++;
|
|
269
|
-
ui.step(`\n🔄 ${stepNumber}/${totalSteps} - ${getT('restore.index.restoringRealtimeSettings')}`);
|
|
270
|
-
await step08RealtimeSettings({
|
|
271
|
-
backupPath: selectedBackup.path,
|
|
272
|
-
targetProject
|
|
273
|
-
});
|
|
274
|
-
restoreResults.realtimeSettings = { success: true };
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// report.json de restauração
|
|
278
|
-
const report = {
|
|
279
|
-
process: 'restore',
|
|
280
|
-
created_at: new Date().toISOString(),
|
|
281
|
-
target_project_id: targetProject.targetProjectId,
|
|
282
|
-
assets: {
|
|
283
|
-
env: path.join(processDir, 'env', '.env.local'),
|
|
284
|
-
env_map: path.join(processDir, 'env', 'env-map.json')
|
|
285
|
-
},
|
|
286
|
-
components: components,
|
|
287
|
-
results: restoreResults,
|
|
288
|
-
notes: [
|
|
289
|
-
'supabase/functions limpo antes e depois do deploy (se Edge Functions selecionado)'
|
|
290
|
-
]
|
|
291
|
-
};
|
|
292
|
-
try {
|
|
293
|
-
fs.writeFileSync(path.join(processDir, 'report.json'), JSON.stringify(report, null, 2));
|
|
294
|
-
} catch {
|
|
295
|
-
// silencioso
|
|
296
|
-
}
|
|
297
|
-
|
|
173
|
+
const remapUrls = await confirm(getT('restore.verified.remapUrls'), true);
|
|
174
|
+
const restoreResults = await runRestore({ selectedBackup, components, targetProject, processDir, postgresMajor:parsedMajor, remapUrls });
|
|
175
|
+
|
|
298
176
|
// Exibir resumo final
|
|
299
177
|
ui.success(`\n🎉 ${getT('restore.index.restoreComplete')}`);
|
|
300
178
|
ui.link(`🎯 ${getT('restore.index.targetProjectFinal', { projectId: targetProject.targetProjectId || getT('restore.index.notConfigured') })}`);
|
|
@@ -310,7 +188,10 @@ module.exports = async (options = {}) => {
|
|
|
310
188
|
}
|
|
311
189
|
|
|
312
190
|
if (restoreResults.authSettings) {
|
|
313
|
-
|
|
191
|
+
if (restoreResults.authSettings.verified)
|
|
192
|
+
ui.success(`🔐 ${getT('restore.index.authSettingsRestored')}`);
|
|
193
|
+
else
|
|
194
|
+
ui.warn(getT('restore.smtp.accepted'));
|
|
314
195
|
}
|
|
315
196
|
|
|
316
197
|
if (restoreResults.storage) {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const { isMasked } = require('../../utils/auth-secrets');
|
|
2
|
+
|
|
3
|
+
async function management(
|
|
4
|
+
target,
|
|
5
|
+
endpoint,
|
|
6
|
+
{ method = 'GET', body, fetchImpl = fetch } = {},
|
|
7
|
+
) {
|
|
8
|
+
if (!/^[a-z0-9-]+$/.test(target.targetProjectId) || !target.targetAccessToken)
|
|
9
|
+
throw new Error('Management API credentials are required');
|
|
10
|
+
let response;
|
|
11
|
+
try {
|
|
12
|
+
response = await fetchImpl(
|
|
13
|
+
`https://api.supabase.com/v1/projects/${target.targetProjectId}/${endpoint}`,
|
|
14
|
+
{
|
|
15
|
+
method,
|
|
16
|
+
signal: AbortSignal.timeout(60000),
|
|
17
|
+
headers: {
|
|
18
|
+
Authorization: `Bearer ${target.targetAccessToken}`,
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
|
+
},
|
|
21
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
} catch {
|
|
25
|
+
throw new Error(`Management API ${endpoint}: network failure`);
|
|
26
|
+
}
|
|
27
|
+
// Error response bodies may contain secrets from the submitted configuration.
|
|
28
|
+
if (!response.ok)
|
|
29
|
+
throw new Error(`Management API ${endpoint}: HTTP ${response.status}`);
|
|
30
|
+
return response.status === 204 ? null : response.json();
|
|
31
|
+
}
|
|
32
|
+
async function applyConfig(
|
|
33
|
+
target,
|
|
34
|
+
endpoint,
|
|
35
|
+
desired,
|
|
36
|
+
allowed,
|
|
37
|
+
request = management,
|
|
38
|
+
{ writeOnly = {} } = {},
|
|
39
|
+
) {
|
|
40
|
+
const secretFields = Object.keys(writeOnly);
|
|
41
|
+
for (const key of secretFields) {
|
|
42
|
+
if (
|
|
43
|
+
endpoint !== 'config/auth' ||
|
|
44
|
+
key !== 'smtp_pass' ||
|
|
45
|
+
!allowed.includes(key)
|
|
46
|
+
)
|
|
47
|
+
throw new Error(`Unsupported write-only configuration: ${key}`);
|
|
48
|
+
if (
|
|
49
|
+
typeof writeOnly[key] !== 'string' ||
|
|
50
|
+
!writeOnly[key].trim() ||
|
|
51
|
+
isMasked(writeOnly[key])
|
|
52
|
+
)
|
|
53
|
+
throw new Error(`A real credential is required: ${key}`);
|
|
54
|
+
}
|
|
55
|
+
if (endpoint === 'config/auth' && Object.hasOwn(desired, 'smtp_pass'))
|
|
56
|
+
throw new Error(
|
|
57
|
+
'SMTP password must be supplied separately, never copied from a configuration response',
|
|
58
|
+
);
|
|
59
|
+
const before = await request(target, endpoint);
|
|
60
|
+
const patch = { ...writeOnly };
|
|
61
|
+
const unsupported = [];
|
|
62
|
+
for (const [key, value] of Object.entries(desired)) {
|
|
63
|
+
if (JSON.stringify(before[key]) === JSON.stringify(value)) continue;
|
|
64
|
+
if (!allowed.includes(key)) {
|
|
65
|
+
unsupported.push(key);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (isMasked(value))
|
|
69
|
+
throw new Error(`Masked configuration cannot be restored: ${key}`);
|
|
70
|
+
patch[key] = value;
|
|
71
|
+
}
|
|
72
|
+
if (unsupported.length)
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Configuration requires manual migration: ${unsupported.join(', ')}`,
|
|
75
|
+
);
|
|
76
|
+
if (Object.keys(patch).length)
|
|
77
|
+
await request(target, endpoint, { method: 'PATCH', body: patch });
|
|
78
|
+
const after = await request(target, endpoint);
|
|
79
|
+
const differences = Object.keys(desired).filter(
|
|
80
|
+
(k) => JSON.stringify(desired[k]) !== JSON.stringify(after[k]),
|
|
81
|
+
);
|
|
82
|
+
if (differences.length)
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Configuration verification failed: ${differences.join(', ')}`,
|
|
85
|
+
);
|
|
86
|
+
return {
|
|
87
|
+
success: true,
|
|
88
|
+
verified: secretFields.length === 0,
|
|
89
|
+
configuration_readback_verified: true,
|
|
90
|
+
write_only_fields_accepted: secretFields,
|
|
91
|
+
...(secretFields.length ? { email_delivery_verified: false } : {}),
|
|
92
|
+
fields_verified: Object.keys(desired).length,
|
|
93
|
+
fields_updated: Object.keys(patch),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
module.exports = { management, applyConfig };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
|
|
3
|
+
const ident = (value) => '"' + String(value).replace(/"/g, '""') + '"';
|
|
4
|
+
const literal = (value) => "'" + String(value).replace(/'/g, "''") + "'";
|
|
5
|
+
|
|
6
|
+
// SQL and credentials go through stdin/environment, never a shell command.
|
|
7
|
+
function runSql(databaseUrl, sql, { postgresMajor = 17 } = {}) {
|
|
8
|
+
const url = new URL(databaseUrl);
|
|
9
|
+
if (!['postgres:', 'postgresql:'].includes(url.protocol))
|
|
10
|
+
throw new Error('Invalid database URL');
|
|
11
|
+
if (![15, 16, 17].includes(Number(postgresMajor)))
|
|
12
|
+
throw new Error('Unsupported PostgreSQL major');
|
|
13
|
+
const env = {
|
|
14
|
+
...process.env,
|
|
15
|
+
PGHOST: url.hostname,
|
|
16
|
+
PGPORT: url.port || '5432',
|
|
17
|
+
PGUSER: decodeURIComponent(url.username),
|
|
18
|
+
PGPASSWORD: decodeURIComponent(url.password),
|
|
19
|
+
PGDATABASE: decodeURIComponent(url.pathname.slice(1) || 'postgres'),
|
|
20
|
+
PGSSLMODE: url.searchParams.get('sslmode') || 'prefer',
|
|
21
|
+
PGCONNECT_TIMEOUT: '20',
|
|
22
|
+
};
|
|
23
|
+
const args = [
|
|
24
|
+
'run',
|
|
25
|
+
'--rm',
|
|
26
|
+
'-i',
|
|
27
|
+
'--network',
|
|
28
|
+
'host',
|
|
29
|
+
...[
|
|
30
|
+
'PGHOST',
|
|
31
|
+
'PGPORT',
|
|
32
|
+
'PGUSER',
|
|
33
|
+
'PGPASSWORD',
|
|
34
|
+
'PGDATABASE',
|
|
35
|
+
'PGSSLMODE',
|
|
36
|
+
'PGCONNECT_TIMEOUT',
|
|
37
|
+
].flatMap((k) => ['-e', k]),
|
|
38
|
+
`postgres:${postgresMajor}`,
|
|
39
|
+
'psql',
|
|
40
|
+
'-X',
|
|
41
|
+
'-q',
|
|
42
|
+
'-A',
|
|
43
|
+
'-t',
|
|
44
|
+
'-v',
|
|
45
|
+
'ON_ERROR_STOP=1',
|
|
46
|
+
'-v',
|
|
47
|
+
'VERBOSITY=sqlstate',
|
|
48
|
+
];
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const child = spawn('docker', args, {
|
|
51
|
+
env,
|
|
52
|
+
windowsHide: true,
|
|
53
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
54
|
+
});
|
|
55
|
+
let output = '';
|
|
56
|
+
let errors = '';
|
|
57
|
+
child.stdout.on('data', (chunk) => {
|
|
58
|
+
output += chunk;
|
|
59
|
+
});
|
|
60
|
+
child.stderr.on('data', (chunk) => {
|
|
61
|
+
errors += chunk;
|
|
62
|
+
});
|
|
63
|
+
child.stdin.on('error', () => {});
|
|
64
|
+
child.on('error', () =>
|
|
65
|
+
reject(new Error('Could not start Docker PostgreSQL client')),
|
|
66
|
+
);
|
|
67
|
+
child.on('close', (code) => {
|
|
68
|
+
if (code !== 0) {
|
|
69
|
+
// COPY errors can contain customer records. Expose SQLSTATE only.
|
|
70
|
+
const states = [
|
|
71
|
+
...new Set(errors.match(/\b[0-9A-Z]{5}\b/g) || []),
|
|
72
|
+
].filter((x) => /\d/.test(x));
|
|
73
|
+
reject(
|
|
74
|
+
new Error(
|
|
75
|
+
`Database operation failed (exit ${code}; SQLSTATE ${states.join(', ') || 'unavailable'}). Transaction rolled back when applicable.`,
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
} else resolve(output.trim());
|
|
79
|
+
});
|
|
80
|
+
child.stdin.end(sql);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
module.exports = { runSql, ident, literal };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { t } = require('../../i18n');
|
|
4
|
+
const { buildRestore, readDump } = require('./dump');
|
|
5
|
+
const { storagePlan } = require('./steps/06-storage');
|
|
6
|
+
const { prepareAuth } = require('./auth');
|
|
7
|
+
const steps = {
|
|
8
|
+
database: require('./steps/03-database'),
|
|
9
|
+
edgeFunctions: require('./steps/04-edge-functions'),
|
|
10
|
+
authSettings: require('./steps/05-auth-settings'),
|
|
11
|
+
storage: require('./steps/06-storage'),
|
|
12
|
+
databaseSettings: require('./steps/07-database-settings'),
|
|
13
|
+
realtimeSettings: require('./steps/08-realtime-settings'),
|
|
14
|
+
};
|
|
15
|
+
async function runRestore(
|
|
16
|
+
{
|
|
17
|
+
selectedBackup,
|
|
18
|
+
components,
|
|
19
|
+
targetProject,
|
|
20
|
+
processDir,
|
|
21
|
+
postgresMajor,
|
|
22
|
+
remapUrls = true,
|
|
23
|
+
},
|
|
24
|
+
implementations = steps,
|
|
25
|
+
{ prepareAuthConfig = prepareAuth } = {},
|
|
26
|
+
) {
|
|
27
|
+
if (selectedBackup.projectId === targetProject.targetProjectId)
|
|
28
|
+
throw new Error('Restore requires a different, empty destination project');
|
|
29
|
+
const context = {
|
|
30
|
+
backupPath: selectedBackup.path,
|
|
31
|
+
backupFilePath: path.join(selectedBackup.path, selectedBackup.backupFile),
|
|
32
|
+
targetProject,
|
|
33
|
+
targetDatabaseUrl: targetProject.targetDatabaseUrl,
|
|
34
|
+
sourceProjectId: selectedBackup.projectId,
|
|
35
|
+
targetProjectId: targetProject.targetProjectId,
|
|
36
|
+
postgresMajor,
|
|
37
|
+
remapUrls,
|
|
38
|
+
};
|
|
39
|
+
const report = {
|
|
40
|
+
process: 'restore',
|
|
41
|
+
smoonb_version: require('../../../package.json').version,
|
|
42
|
+
created_at: new Date().toISOString(),
|
|
43
|
+
source_project_id: selectedBackup.projectId,
|
|
44
|
+
target_project_id: targetProject.targetProjectId,
|
|
45
|
+
status: 'running',
|
|
46
|
+
components,
|
|
47
|
+
results: {},
|
|
48
|
+
remap_service_urls: remapUrls,
|
|
49
|
+
notes: [
|
|
50
|
+
'Local source files and backup archives are preserved.',
|
|
51
|
+
'Verification covers selected components; application login and workflows require acceptance testing.',
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
const save = () =>
|
|
55
|
+
fs.writeFileSync(
|
|
56
|
+
path.join(processDir, 'report.json'),
|
|
57
|
+
JSON.stringify(report, null, 2),
|
|
58
|
+
);
|
|
59
|
+
save();
|
|
60
|
+
let current = 'preflight';
|
|
61
|
+
try {
|
|
62
|
+
if (targetProject.targetDatabaseUrl) {
|
|
63
|
+
const url = new URL(targetProject.targetDatabaseUrl);
|
|
64
|
+
const hosted =
|
|
65
|
+
url.hostname.endsWith('.supabase.co') ||
|
|
66
|
+
url.hostname.endsWith('.supabase.com');
|
|
67
|
+
if (
|
|
68
|
+
hosted &&
|
|
69
|
+
url.hostname !== `db.${targetProject.targetProjectId}.supabase.co` &&
|
|
70
|
+
!decodeURIComponent(url.username).endsWith(
|
|
71
|
+
`.${targetProject.targetProjectId}`,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
throw new Error(
|
|
75
|
+
'Database connection does not match the destination project',
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (
|
|
79
|
+
targetProject.targetUrl &&
|
|
80
|
+
new URL(targetProject.targetUrl).hostname !==
|
|
81
|
+
`${targetProject.targetProjectId}.supabase.co`
|
|
82
|
+
)
|
|
83
|
+
throw new Error('API URL does not match the destination project');
|
|
84
|
+
// Parse and validate inventories before the first mutation.
|
|
85
|
+
if (components.database)
|
|
86
|
+
buildRestore(readDump(context.backupFilePath), context);
|
|
87
|
+
if (components.storage)
|
|
88
|
+
storagePlan(context.backupPath, context.backupFilePath);
|
|
89
|
+
// Resolve write-only credentials before restoring the database or any service.
|
|
90
|
+
if (components.authSettings)
|
|
91
|
+
context.authConfig = await prepareAuthConfig(context);
|
|
92
|
+
for (const [name, step] of Object.entries(implementations)) {
|
|
93
|
+
if (!components[name]) continue;
|
|
94
|
+
current = name;
|
|
95
|
+
report.results[name] = { success: false, status: 'running' };
|
|
96
|
+
save();
|
|
97
|
+
const labels = {
|
|
98
|
+
database: 'restoringDatabase',
|
|
99
|
+
edgeFunctions: 'restoringEdgeFunctions',
|
|
100
|
+
authSettings: 'restoringAuthSettings',
|
|
101
|
+
storage: 'restoringStorageBuckets',
|
|
102
|
+
databaseSettings: 'restoringDatabaseSettings',
|
|
103
|
+
realtimeSettings: 'restoringRealtimeSettings',
|
|
104
|
+
};
|
|
105
|
+
console.log((global.smoonbI18n?.t || t)(`restore.index.${labels[name]}`));
|
|
106
|
+
const result = await step(context);
|
|
107
|
+
const acceptedSmtp =
|
|
108
|
+
name === 'authSettings' &&
|
|
109
|
+
result?.configuration_readback_verified === true &&
|
|
110
|
+
result?.email_delivery_verified === false &&
|
|
111
|
+
result?.write_only_fields_accepted?.length === 1 &&
|
|
112
|
+
result.write_only_fields_accepted[0] === 'smtp_pass';
|
|
113
|
+
if (!result?.success || (!result?.verified && !acceptedSmtp))
|
|
114
|
+
throw new Error(`Component ${name} did not pass verification`);
|
|
115
|
+
report.results[name] = {
|
|
116
|
+
...result,
|
|
117
|
+
status: result.verified
|
|
118
|
+
? 'verified'
|
|
119
|
+
: 'applied_with_unverified_secrets',
|
|
120
|
+
};
|
|
121
|
+
save();
|
|
122
|
+
if (acceptedSmtp)
|
|
123
|
+
console.log((global.smoonbI18n?.t || t)('restore.smtp.accepted'));
|
|
124
|
+
}
|
|
125
|
+
report.status = Object.values(report.results).every((r) => r.verified)
|
|
126
|
+
? 'verified'
|
|
127
|
+
: 'applied_with_unverified_secrets';
|
|
128
|
+
report.completed_at = new Date().toISOString();
|
|
129
|
+
save();
|
|
130
|
+
return report.results;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
report.status = 'failed';
|
|
133
|
+
report.failed_component = current;
|
|
134
|
+
report.results[current] = { success: false, status: 'failed' };
|
|
135
|
+
report.completed_at = new Date().toISOString();
|
|
136
|
+
save();
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
module.exports = { runRestore };
|
|
@@ -1,84 +1,18 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
console.log(chalk.white(` - ${getT('restore.steps.database.detectedGz')}`));
|
|
20
|
-
console.log(chalk.white(` - ${getT('restore.steps.database.extractingGz')}`));
|
|
21
|
-
|
|
22
|
-
const unzipCmd = [
|
|
23
|
-
'docker run --rm',
|
|
24
|
-
`-v "${backupDirAbs}:/host"`,
|
|
25
|
-
'postgres:17 gunzip /host/' + fileName
|
|
26
|
-
].join(' ');
|
|
27
|
-
|
|
28
|
-
execSync(unzipCmd, { stdio: 'pipe' });
|
|
29
|
-
uncompressedFile = fileName.replace('.gz', '');
|
|
30
|
-
console.log(chalk.white(` - ${getT('restore.steps.database.uncompressed', { file: uncompressedFile })}`));
|
|
31
|
-
} else if (fileName.endsWith('.backup')) {
|
|
32
|
-
console.log(chalk.white(` - ${getT('restore.steps.database.detectedBackup')}`));
|
|
33
|
-
console.log(chalk.white(` - ${getT('restore.steps.database.proceeding')}`));
|
|
34
|
-
} else {
|
|
35
|
-
throw new Error(`Formato de arquivo inválido. Esperado .backup.gz ou .backup, recebido: ${fileName}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Extrair credenciais da URL de conexão
|
|
39
|
-
const urlMatch = targetDatabaseUrl.match(/postgresql:\/\/([^@:]+):([^@]+)@(.+)$/);
|
|
40
|
-
|
|
41
|
-
if (!urlMatch) {
|
|
42
|
-
const getT = global.smoonbI18n?.t || t;
|
|
43
|
-
throw new Error(getT('error.databaseUrlInvalid'));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Comando psql conforme documentação oficial Supabase
|
|
47
|
-
const restoreCmd = [
|
|
48
|
-
'docker run --rm --network host',
|
|
49
|
-
`-v "${backupDirAbs}:/host"`,
|
|
50
|
-
`-e PGPASSWORD="${encodeURIComponent(urlMatch[2])}"`,
|
|
51
|
-
'postgres:17 psql',
|
|
52
|
-
`-d "${targetDatabaseUrl}"`,
|
|
53
|
-
`-f /host/${uncompressedFile}`
|
|
54
|
-
].join(' ');
|
|
55
|
-
|
|
56
|
-
console.log(chalk.cyan(` - ${getT('restore.steps.database.executing')}`));
|
|
57
|
-
console.log(chalk.cyan(` ℹ️ ${getT('restore.steps.database.followingDocs')}`));
|
|
58
|
-
console.log(chalk.yellow(` ⚠️ ${getT('restore.steps.database.warning')}`));
|
|
59
|
-
console.log(chalk.yellow(` ⚠️ ${getT('restore.steps.database.warningReason1')}`));
|
|
60
|
-
console.log(chalk.yellow(` ⚠️ ${getT('restore.steps.database.warningReason2')}`));
|
|
61
|
-
|
|
62
|
-
// Executar comando de restauração
|
|
63
|
-
execSync(restoreCmd, { stdio: 'inherit', encoding: 'utf8' });
|
|
64
|
-
|
|
65
|
-
console.log(chalk.green(` ✅ ${getT('restore.steps.database.success')}`));
|
|
66
|
-
ui.hint(` ℹ️ ${getT('restore.steps.database.normalErrors')}`);
|
|
67
|
-
|
|
68
|
-
} catch (error) {
|
|
69
|
-
// Erros esperados conforme documentação oficial Supabase
|
|
70
|
-
const getT = global.smoonbI18n?.t || t;
|
|
71
|
-
if (error.message.includes('already exists') ||
|
|
72
|
-
error.message.includes('constraint') ||
|
|
73
|
-
error.message.includes('duplicate') ||
|
|
74
|
-
error.stdout?.includes('already exists')) {
|
|
75
|
-
console.log(chalk.yellow(` ⚠️ ${getT('restore.steps.database.expectedErrors')}`));
|
|
76
|
-
console.log(chalk.green(` ✅ ${getT('restore.steps.database.success')}`));
|
|
77
|
-
ui.hint(` ℹ️ ${getT('restore.steps.database.errorsIgnored')}`);
|
|
78
|
-
} else {
|
|
79
|
-
console.error(chalk.red(` ❌ ${getT('restore.steps.database.error', { message: error.message })}`));
|
|
80
|
-
throw error;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
|
|
1
|
+
const { readDump, buildRestore } = require('../dump');
|
|
2
|
+
const { runSql } = require('../pg');
|
|
3
|
+
module.exports = async ({
|
|
4
|
+
backupFilePath,
|
|
5
|
+
targetDatabaseUrl,
|
|
6
|
+
sourceProjectId,
|
|
7
|
+
targetProjectId,
|
|
8
|
+
remapUrls = true,
|
|
9
|
+
postgresMajor = 17,
|
|
10
|
+
}) => {
|
|
11
|
+
const plan = buildRestore(readDump(backupFilePath), {
|
|
12
|
+
sourceProjectId,
|
|
13
|
+
targetProjectId,
|
|
14
|
+
remapUrls,
|
|
15
|
+
});
|
|
16
|
+
await runSql(targetDatabaseUrl, plan.sql, { postgresMajor });
|
|
17
|
+
return plan.summary;
|
|
18
|
+
};
|