smoonb 1.0.7 → 1.0.8

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 CHANGED
@@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [1.0.8] - 2026-10-10
8
+
9
+ ### Fixed
10
+
11
+ - Restore application and Auth data from the existing full dump in one checked transaction. Fail on SQL errors; compare restored columns and validate foreign keys before commit, including pre-existing Auth constraints.
12
+ - Remove extra destination object grants before replaying source ACLs, preventing unintended anonymous/authenticated access to restricted functions and tables.
13
+ - Apply and verify Auth configuration through the Management API, including SMTP and redirect configuration.
14
+ - Remap source Supabase hostnames in application data and text objects when selected; preserve unrelated IDs and paths.
15
+ - Recover Storage MIME types, cache directives, ownership and user metadata from the full dump even when legacy Storage JSON inventories are empty. Validate ZIP inventory, count empty buckets, and verify every uploaded file by download and SHA-256.
16
+ - Use isolated temporary workspaces for Edge Function backup and deployment; preserve local modified sources and compressed backup archives. Address npm CLI shims on Windows without shell interpolation.
17
+ - Persist stage results and failures; never report unsuccessful or unverified components as restored.
18
+
19
+ ### Changed
20
+
21
+ - Apply and verify Realtime snapshots through the API; capture fresh Realtime settings for new backups instead of reusing interactive recordings.
22
+ - Verify database settings and fail explicitly for incompatible platform settings rather than silently skipping them.
23
+ - Require Node.js 20 or later. A new, empty destination is required for the database restore; platform-managed schemas and administrator defaults remain destination-managed.
24
+ - Preserve Function JWT configuration in new backups. Legacy backups default to requiring JWT.
25
+
26
+ ### Tests
27
+
28
+ - Add regression coverage for configuration readback, failure reporting, URL mapping, archive preservation, isolated Functions, and PostgreSQL arrays.
29
+ - Add an opt-in Docker test of the actual restore step with cyclic references, Auth identities, broad destination defaults, invalid-data rollback, populated-target refusal and Storage metadata/content verification (mock HTTP, real PostgreSQL).
30
+
31
+ ---
32
+
7
33
  ## [1.0.7] - 2026-10-10
8
34
 
9
35
  ### Fixed
package/README.md CHANGED
@@ -296,14 +296,28 @@ npx smoonb restore --skip-supabase-version-check
296
296
  - 🔄 Realtime Settings (if available in backup)
297
297
  9. **Detailed summary** - Shows selected backup, target project and components
298
298
  10. **Final confirmation** - Confirms before starting
299
- 11. **Disclaimer** - Explains that errors during restore are expected; link to [Supabase Dashboard Restore docs](https://supabase.com/docs/guides/platform/migrating-within-supabase/dashboard-restore); you should wait for the process to finish and then test the result before accepting to proceed
299
+ 11. **Project URLs** - Confirms whether Supabase URLs in data and files should point to the new project
300
300
  12. **Restore execution:**
301
301
  - 📊 Database - Restores via `psql` (supports `.backup.gz` and `.backup`)
302
- - ⚡ Edge Functions - Copies and deploys to target project
303
- - 🔐 Auth Settings - Displays configurations for manual application
304
- - 📦 Storage - Restores buckets and files from ZIP (if available) or displays information for manual migration
302
+ - ⚡ Edge Functions - Deploys from a temporary copy, preserving local sources
303
+ - 🔐 Auth Settings - Applies via API and verifies the resulting values
304
+ - 📦 Storage - Restores buckets and ZIP files, preserves metadata and verifies every download
305
305
  - 🔧 Database Settings - Restores extensions and settings via SQL
306
- - 🔄 Realtime Settings - Displays configurations for manual application
306
+ - 🔄 Realtime Settings - Applies and verifies the snapshot recorded in the backup
307
+
308
+ ### Verified restoration (1.0.8)
309
+
310
+ Use a new, empty destination project and Node.js 20 or later. Existing complete 1.0.7 backups can be reused without another capture. Version 1.0.8 uses the complete dump as one snapshot for application structure, Auth data, migration history, buckets and object metadata. Platform schemas and internal migrations remain at the destination's version.
311
+
312
+ Database loading uses one transaction, stops on errors, suspends triggers only on its connection, compares every restored column and checks foreign keys before commit. Extra destination default grants are cleared before replaying the backup's grants. Failed SQL is never reported as success. Compressed archives and local function sources remain untouched.
313
+
314
+ Optional URL remapping changes only the project's Supabase hostname, including URLs inside JSON and text files. It does not rename files or replace isolated project IDs. Historical URLs may also change; choose preservation when needed. Signed URLs, custom domains, external integrations and application API keys need separate validation. Destination platform keys are not replaced.
315
+
316
+ Auth and Realtime are applied through the Management API and verified by reading them back. Legacy Realtime captures may be stale: restoration reproduces the recorded values and date, not the source project's current settings. New backups capture Realtime directly through the API. Legacy function backups without the JWT flag default to requiring JWT; new backups preserve that flag.
317
+
318
+ Reports are saved after each stage, including failures. External services cannot share the database transaction: earlier stages remain completed if a later stage fails. Do not reload the database into a populated destination. Resolve the reported stage and select only pending components, or use another empty project.
319
+
320
+ Unsupported internal data (such as encrypted Vault secrets without key migration) and destination plan/version incompatibilities stop the operation. Verified status covers the selected components; validate login, email delivery and business workflows before production use.
307
321
 
308
322
  **Supported file formats:**
309
323
  - ✅ `.backup.gz` (compressed) - Automatically decompresses before restoring
@@ -503,7 +517,7 @@ restore/
503
517
  npx smoonb restore
504
518
  ```
505
519
  (Select desired backup, select components, execute restoration.)
506
- 6. **Apply manual configurations** if necessary: Auth Settings (Dashboard → Authentication → Settings), Realtime (Dashboard → Database → Replication). Storage is restored automatically from ZIP when available.
520
+ 6. **Review the result**: inspect `backups/restore-*/report.json` and test login, email and application workflows on the destination.
507
521
 
508
522
  ## 🎨 User Experience
509
523
 
@@ -602,7 +616,7 @@ npx smoonb restore --skip-supabase-version-check
602
616
  ### .backup.gz file cannot be restored
603
617
 
604
618
  smoonb automatically supports:
605
- - ✅ `.backup.gz` - Decompresses via Docker before restoring
619
+ - ✅ `.backup.gz` - Reads compressed content without changing the original archive
606
620
  - ✅ `.backup` - Restores directly
607
621
 
608
622
  If there are problems:
@@ -661,3 +675,5 @@ Contributions are welcome. [www.smoonb.com/#price](https://www.smoonb.com/#price
661
675
  **Developed by:** Goalmoon Tecnologia LTDA
662
676
  **Website:** https://www.smoonb.com
663
677
  **GitHub:** https://github.com/almmello/smoonb
678
+
679
+ Additional restore-path test: `npm run test:restore:docker`. Uses disposable PostgreSQL, synthetic data and mocked Storage HTTP transport; covers Auth, grants, cycles, rollback, files and metadata.
package/README.pt-BR.md CHANGED
@@ -296,14 +296,28 @@ npx smoonb restore --skip-supabase-version-check
296
296
  - 🔄 Realtime Settings (se disponível no backup)
297
297
  9. **Resumo detalhado** - Mostra backup selecionado, projeto destino e componentes
298
298
  10. **Confirmação final** - Confirma antes de iniciar
299
- 11. **Aviso** - Explica que erros durante a restauração são esperados; link para [documentação de restore do Dashboard Supabase](https://supabase.com/docs/guides/platform/migrating-within-supabase/dashboard-restore); aguardar o fim do processo e testar o resultado antes de aceitar para prosseguir
299
+ 11. **Endereços do projeto** - Confirma se as URLs Supabase nos dados e arquivos devem apontar para o novo projeto
300
300
  12. **Execução da restauração:**
301
301
  - 📊 Database - Restaura via `psql` (suporta `.backup.gz` e `.backup`)
302
- - ⚡ Edge Functions - Copia e faz deploy no projeto destino
303
- - 🔐 Auth Settings - Exibe configurações para aplicação manual
304
- - 📦 Storage - Restaura buckets e arquivos do ZIP (se disponível) ou exibe informações para migração manual
302
+ - ⚡ Edge Functions - Faz deploy de uma cópia temporária, preservando as fontes locais
303
+ - 🔐 Auth Settings - Aplica via API e verifica os valores recuperados
304
+ - 📦 Storage - Restaura buckets e arquivos do ZIP, preserva metadados e verifica cada download
305
305
  - 🔧 Database Settings - Restaura extensões e configurações via SQL
306
- - 🔄 Realtime Settings - Exibe configurações para aplicação manual
306
+ - 🔄 Realtime Settings - Aplica e verifica o snapshot registrado no backup
307
+
308
+ ### Restauração verificada (1.0.8)
309
+
310
+ Use um projeto de destino novo e vazio e Node.js 20 ou mais recente. O backup completo existente da versão 1.0.7 pode ser reutilizado, sem nova captura. A versão 1.0.8 usa o dump completo como snapshot único: recupera a estrutura da aplicação, dados de Auth, histórico de migrações, buckets e metadados. Os schemas internos e as migrações da plataforma são mantidos na versão do destino.
311
+
312
+ A carga do banco usa uma transação com interrupção em erro, suspende triggers apenas nessa conexão, compara todas as colunas recuperadas e verifica chaves estrangeiras antes de confirmar. Permissões herdadas dos defaults do destino são removidas antes da reaplicação dos grants do backup. SQL com falha nunca é classificado como sucesso. O arquivo compactado e a pasta local de funções são preservados.
313
+
314
+ O remapeamento opcional troca apenas o domínio Supabase do projeto em URLs, inclusive em JSON e arquivos de texto. Não troca IDs isolados nem nomes de arquivos. URLs históricas também podem ser afetadas; escolha preservar quando isso for necessário. URLs assinadas, domínios personalizados, integrações externas e chaves de API da aplicação precisam de validação específica. As chaves de plataforma do destino não são substituídas.
315
+
316
+ Auth e Realtime são aplicados pela Management API e conferidos por nova leitura. A captura antiga de Realtime pode conter valores desatualizados: a restauração reproduz os valores e a data registrados, não o estado atual do projeto original. Novos backups capturam Realtime diretamente pela API. Em backups antigos sem o sinalizador de JWT de Functions, o padrão seguro é exigir JWT; os novos backups guardam esse sinalizador.
317
+
318
+ O relatório é salvo durante cada etapa, inclusive em falhas. Serviços externos não participam da transação do banco: se uma etapa posterior falhar, as anteriores permanecem registradas como concluídas. Não repita a carga do banco em um destino já preenchido. Resolva a etapa indicada e selecione apenas os componentes pendentes, ou use outro projeto vazio.
319
+
320
+ Dados internos não suportados (por exemplo, Vault com segredos criptografados sem migração da chave) e configurações incompatíveis com o plano/versão do destino interrompem o processo. O status verificado cobre os componentes selecionados; ainda é necessário validar login, envio de e-mail e fluxos de negócio antes de usar o destino em produção.
307
321
 
308
322
  **Formato de arquivos suportados:**
309
323
  - ✅ `.backup.gz` (compactado) - Descompacta automaticamente antes de restaurar
@@ -503,7 +517,7 @@ restore/
503
517
  npx smoonb restore
504
518
  ```
505
519
  (Seleciona backup desejado, componentes a restaurar, executa restauração.)
506
- 6. **Aplicar configurações manuais** se necessário: Auth Settings (Dashboard → Authentication → Settings), Realtime (Dashboard → Database → Replication). O Storage é restaurado automaticamente do ZIP quando disponível.
520
+ 6. **Conferir o resultado**: leia `backups/restore-*/report.json` e teste login, e-mail e os fluxos da aplicação no destino.
507
521
 
508
522
  ## 🎨 Experiência do Usuário
509
523
 
@@ -602,7 +616,7 @@ npx smoonb restore --skip-supabase-version-check
602
616
  ### Arquivo .backup.gz não pode ser restaurado
603
617
 
604
618
  O smoonb suporta automaticamente:
605
- - ✅ `.backup.gz` - Descompacta via Docker antes de restaurar
619
+ - ✅ `.backup.gz` - Lê o conteúdo compactado sem modificar o arquivo original
606
620
  - ✅ `.backup` - Restaura diretamente
607
621
 
608
622
  Se houver problemas:
@@ -661,3 +675,5 @@ Contribuições são bem-vindas. [www.smoonb.com/#price](https://www.smoonb.com/
661
675
  **Desenvolvido por:** Goalmoon Tecnologia LTDA
662
676
  **Website:** https://www.smoonb.com
663
677
  **GitHub:** https://github.com/almmello/smoonb
678
+
679
+ Teste adicional do caminho real de restore: `npm run test:restore:docker`. Usa PostgreSQL descartável, dados fictícios e transporte HTTP simulado para Storage; cobre Auth, permissões, ciclos, rollback, arquivos e metadados.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoonb",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "Complete Supabase backup and migration tool. https://www.smoonb.com/#price",
5
5
  "preferGlobal": false,
6
6
  "preventGlobalInstall": true,
@@ -18,7 +18,8 @@
18
18
  "build": "node scripts/build.js",
19
19
  "build:check": "node scripts/build.js",
20
20
  "prepublishOnly": "npm test && npm run lint && npm run build",
21
- "test:docker": "node tests/integration/circular-restore.js"
21
+ "test:docker": "node tests/integration/circular-restore.js",
22
+ "test:restore:docker": "node tests/integration/verified-restore.js"
22
23
  },
23
24
  "keywords": [
24
25
  "supabase",
@@ -33,7 +34,7 @@
33
34
  "author": "Goalmoon Tecnologia LTDA <https://www.smoonb.com>",
34
35
  "license": "SEE LICENSE IN LICENSE.md",
35
36
  "engines": {
36
- "node": ">=16.0.0"
37
+ "node": ">=20.0.0"
37
38
  },
38
39
  "dependencies": {
39
40
  "@supabase/supabase-js": "^2.93.0",
@@ -1,33 +1,19 @@
1
- const chalk = require('chalk');
2
- const path = require('path');
3
- const fs = require('fs').promises;
4
- const { captureRealtimeSettings } = require('../../../utils/realtime-settings');
5
- const { t } = require('../../../i18n');
6
-
7
- /**
8
- * Etapa 5: Backup Realtime Settings via Captura Interativa
9
- */
10
- module.exports = async ({ projectId, backupDir, options }) => {
11
- try {
12
- const getT = global.smoonbI18n?.t || t;
13
- console.log(chalk.white(` - ${getT('backup.steps.realtime.capturing')}`));
14
-
15
- const result = await captureRealtimeSettings(projectId, backupDir, options?.skipRealtime);
16
-
17
- const settings = result.settings !== undefined ? result.settings : result;
18
- const source = result.source || 'interactive';
19
- const reusedFrom = result.reusedFrom || null;
20
-
21
- const stats = await fs.stat(path.join(backupDir, 'realtime-settings.json'));
22
- const sizeKB = (stats.size / 1024).toFixed(1);
23
-
24
- console.log(chalk.green(` ✅ Realtime Settings capturadas: ${sizeKB} KB`));
25
-
26
- return { success: true, settings, source, reusedFrom };
27
- } catch (error) {
28
- const getT = global.smoonbI18n?.t || t;
29
- console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.realtime.error', { message: error.message })}`));
30
- return { success: false };
31
- }
32
- };
33
-
1
+ const path = require('path');
2
+ const fs = require('fs').promises;
3
+ const { management } = require('../../restore/management');
4
+ module.exports = async ({ projectId, accessToken, backupDir }) => {
5
+ try {
6
+ const settings = await management(
7
+ { targetProjectId: projectId, targetAccessToken: accessToken },
8
+ 'config/realtime',
9
+ );
10
+ const captured_at = new Date().toISOString();
11
+ await fs.writeFile(
12
+ path.join(backupDir, 'realtime-settings.json'),
13
+ JSON.stringify({ project_id: projectId, captured_at, settings }, null, 2),
14
+ );
15
+ return { success: true, settings, source: 'management-api', captured_at };
16
+ } catch {
17
+ return { success: false, reason: 'realtime_api_capture_failed' };
18
+ }
19
+ };
@@ -106,18 +106,13 @@ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supaba
106
106
  try {
107
107
  console.log(chalk.white(` - ${getT('backup.steps.storage.processing', { bucketName: bucket.name })}`));
108
108
 
109
- const objectsResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/storage/buckets/${bucket.name}/objects`, {
110
- headers: {
111
- 'Authorization': `Bearer ${accessToken}`,
112
- 'Content-Type': 'application/json'
113
- }
114
- });
115
-
116
- let objects = [];
117
- if (objectsResponse.ok) {
118
- objects = await objectsResponse.json();
119
- }
120
-
109
+ const { readDump, records } = require('../../restore/dump');
110
+ const dumpFile = (await fs.readdir(backupDir)).find(f => /\.backup(?:\.gz)?$/.test(f));
111
+ if (!dumpFile) throw new Error('Database inventory required for Storage metadata');
112
+ const dump = readDump(path.join(backupDir, dumpFile));
113
+ const objects = records(dump.tables.find(t => t.schema === 'storage' && t.name === 'objects'))
114
+ .filter(o => o.bucket_id === bucket.id).map(o => ({ ...o, metadata: JSON.parse(o.metadata || '{}') }));
115
+
121
116
  const bucketInfo = {
122
117
  id: bucket.id,
123
118
  name: bucket.name,
@@ -1,202 +1,76 @@
1
- const chalk = require('chalk');
2
- const path = require('path');
3
- const fs = require('fs').promises;
4
- const { ensureDir, writeJson } = require('../../../utils/fsx');
5
- const { extractPasswordFromDbUrl, ensureCleanLink } = require('../../../utils/supabaseLink');
6
- const { cleanDir } = require('../../../utils/fsExtra');
7
- const { t } = require('../../../i18n');
8
-
9
- /**
10
- * Etapa 8: Backup Edge Functions via Docker (reset link + limpeza opcional)
11
- */
12
- module.exports = async (context) => {
13
- const { projectId, accessToken, databaseUrl, backupDir } = context;
14
- try {
15
- const functionsDir = path.join(backupDir, 'edge-functions');
16
- await ensureDir(functionsDir);
17
-
18
- // Reset de link ao projeto de ORIGEM
19
- const dbPassword = extractPasswordFromDbUrl(databaseUrl);
20
- await ensureCleanLink(projectId, accessToken, dbPassword);
21
-
22
- // Preparar diretório supabase/functions (criar se não existir, mas não limpar ainda)
23
- const supabaseFunctionsDir = path.join(process.cwd(), 'supabase', 'functions');
24
-
25
- // Verificar flag de limpeza antes do backup
26
- const shouldCleanAfter = context?.cleanupFlags?.cleanFunctions || false;
27
-
28
- // Registrar funções que já existiam ANTES do processo (para preservar se necessário)
29
- let existingFunctionsBefore = [];
30
- try {
31
- const existingItems = await fs.readdir(supabaseFunctionsDir);
32
- for (const item of existingItems) {
33
- const itemPath = path.join(supabaseFunctionsDir, item);
34
- const stats = await fs.stat(itemPath);
35
- if (stats.isDirectory()) {
36
- existingFunctionsBefore.push(item);
37
- }
38
- }
39
- } catch {
40
- // Diretório não existe, tudo bem
41
- existingFunctionsBefore = [];
42
- }
43
-
44
- // Se o usuário escolheu limpar APÓS, podemos limpar ANTES também para garantir ambiente limpo
45
- // Mas se escolheu NÃO limpar, preservamos o que já existe
46
- const getT = global.smoonbI18n?.t || t;
47
-
48
- if (shouldCleanAfter) {
49
- // Limpar antes se o usuário escolheu limpar após (garante ambiente limpo)
50
- await cleanDir(supabaseFunctionsDir);
51
- console.log(chalk.white(` - ${getT('backup.steps.functions.cleanBefore')}`));
52
- } else {
53
- // Apenas garantir que o diretório existe
54
- await fs.mkdir(supabaseFunctionsDir, { recursive: true });
55
- if (existingFunctionsBefore.length > 0) {
56
- console.log(chalk.white(` - ${getT('backup.steps.functions.preserving', { count: existingFunctionsBefore.length })}`));
57
- } else {
58
- console.log(chalk.white(` - ${getT('backup.steps.functions.prepared')}`));
59
- }
60
- }
61
-
62
- console.log(chalk.white(` - ${getT('backup.steps.functions.listing')}`));
63
-
64
- // Usar fetch direto para Management API com Personal Access Token
65
- const functionsResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/functions`, {
66
- headers: {
67
- 'Authorization': `Bearer ${accessToken}`,
68
- 'Content-Type': 'application/json'
69
- }
70
- });
71
-
72
- if (!functionsResponse.ok) {
73
- console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.functions.listError', { status: functionsResponse.status, statusText: functionsResponse.statusText })}`));
74
- return { success: false, reason: 'api_error', functions: [] };
75
- }
76
-
77
- const functions = await functionsResponse.json();
78
-
79
- if (!functions || functions.length === 0) {
80
- console.log(chalk.white(` - ${getT('backup.steps.functions.noneFound')}`));
81
- await writeJson(path.join(functionsDir, 'README.md'), {
82
- message: getT('backup.steps.functions.noFunctionsMessage')
83
- });
84
- return { success: true, reason: 'no_functions', functions: [] };
85
- }
86
-
87
- console.log(chalk.white(` - Encontradas ${functions.length} Edge Function(s)`));
88
-
89
- const downloadedFunctions = [];
90
- let successCount = 0;
91
- let errorCount = 0;
92
- const totalFuncs = functions.length;
93
- const stepStart = Date.now();
94
-
95
- // Baixar cada Edge Function via Supabase CLI
96
- // Nota: O CLI ignora o cwd e sempre baixa para supabase/functions
97
- for (let idx = 0; idx < functions.length; idx++) {
98
- const func = functions[idx];
99
- const current = idx + 1;
100
- const elapsed = Math.floor((Date.now() - stepStart) / 1000);
101
- try {
102
- console.log(chalk.white(` - Baixando ${current}/${totalFuncs}: ${func.name}... (${elapsed}s)`));
103
-
104
- // Criar diretório da função NO BACKUP
105
- const functionTargetDir = path.join(functionsDir, func.name);
106
- await ensureDir(functionTargetDir);
107
-
108
- // Diretório temporário onde o supabase CLI irá baixar (supabase/functions)
109
- const tempDownloadDir = path.join(process.cwd(), 'supabase', 'functions', func.name);
110
-
111
- // Baixar Edge Function via Supabase CLI (sempre vai para supabase/functions)
112
- const { execSync } = require('child_process');
113
-
114
- execSync(`supabase functions download ${func.name}`, {
115
- timeout: 60000,
116
- stdio: 'pipe'
117
- });
118
-
119
- // COPIAR arquivos de supabase/functions para o backup
120
- try {
121
- const stat = await fs.stat(tempDownloadDir);
122
- if (stat.isDirectory()) {
123
- const files = await fs.readdir(tempDownloadDir);
124
- for (const file of files) {
125
- const srcPath = path.join(tempDownloadDir, file);
126
- const dstPath = path.join(functionTargetDir, file);
127
-
128
- const fileStats = await fs.stat(srcPath);
129
- if (fileStats.isDirectory()) {
130
- // Copiar diretórios recursivamente
131
- await fs.cp(srcPath, dstPath, { recursive: true });
132
- } else {
133
- // Copiar arquivos
134
- await fs.copyFile(srcPath, dstPath);
135
- }
136
- }
137
- }
138
- } catch {
139
- // Arquivos não foram baixados, continuar
140
- console.log(chalk.yellow(` ⚠️ Nenhum arquivo encontrado em ${tempDownloadDir}`));
141
- }
142
-
143
- // Limpar função baixada temporariamente APENAS se o usuário escolheu limpar após
144
- // Se não escolheu, preservar (pode ser que já existisse antes)
145
- if (shouldCleanAfter) {
146
- try {
147
- await fs.rm(tempDownloadDir, { recursive: true, force: true });
148
- } catch {
149
- // Ignorar erro de limpeza
150
- }
151
- }
152
- // Se shouldCleanAfter = false, manter a função baixada (e qualquer função que já existia)
153
-
154
- console.log(chalk.green(` ✅ ${func.name} baixada com sucesso`));
155
- successCount++;
156
-
157
- downloadedFunctions.push({
158
- name: func.name,
159
- slug: func.name,
160
- version: func.version || 'unknown',
161
- files: await fs.readdir(functionTargetDir).catch(() => [])
162
- });
163
-
164
- } catch (error) {
165
- console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.functions.downloadError', { funcName: func.name, message: error.message })}`));
166
- errorCount++;
167
- }
168
- }
169
-
170
- console.log(chalk.green(`📊 Backup de Edge Functions concluído:`));
171
- console.log(chalk.green(` ✅ Sucessos: ${successCount}`));
172
- console.log(chalk.green(` ❌ Erros: ${errorCount}`));
173
-
174
- // Limpar pasta supabase/functions APÓS o backup apenas se o usuário escolheu
175
- // Nota: shouldCleanAfter já foi definido acima
176
- if (shouldCleanAfter) {
177
- await cleanDir(supabaseFunctionsDir);
178
- console.log(chalk.white(` - ${getT('backup.steps.functions.cleanAfter')}`));
179
- } else {
180
- // Preservar tudo: tanto as funções que já existiam quanto as que foram baixadas
181
- // As funções baixadas não foram removidas individualmente (linha acima foi ajustada)
182
- console.log(chalk.white(` - ${getT('backup.steps.functions.preserved')}`));
183
- }
184
-
185
- return {
186
- success: true,
187
- reason: 'success',
188
- functions: downloadedFunctions,
189
- functions_count: functions.length,
190
- success_count: successCount,
191
- error_count: errorCount,
192
- method: 'docker'
193
- };
194
-
195
- } catch (error) {
196
- const getT = global.smoonbI18n?.t || t;
197
- console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.functions.error', { message: error.message })}`));
198
- console.log('⏭️ Continuando com outros componentes...');
199
- return { success: false, reason: 'download_error', error: error.message, functions: [] };
200
- }
201
- };
202
-
1
+ const path = require('path');
2
+ const os = require('os');
3
+ const fs = require('fs').promises;
4
+ const { runSupabase } = require('../../../utils/supabaseCli');
5
+ const { management } = require('../../restore/management');
6
+ const { copyDirectoryRecursive } = require('../../restore/utils');
7
+ module.exports = async ({ projectId, accessToken, backupDir }) => {
8
+ const root = await fs.mkdtemp(
9
+ path.join(os.tmpdir(), 'smoonb-functions-backup-'),
10
+ );
11
+ try {
12
+ const functions = await management(
13
+ { targetProjectId: projectId, targetAccessToken: accessToken },
14
+ 'functions',
15
+ );
16
+ await fs.mkdir(path.join(root, 'supabase'), { recursive: true });
17
+ await fs.writeFile(
18
+ path.join(root, 'supabase', 'config.toml'),
19
+ 'project_id = "smoonb-backup"\n',
20
+ );
21
+ const downloaded = [];
22
+ for (const f of functions) {
23
+ const name = f.slug || f.name;
24
+ if (!/^[-a-zA-Z0-9_]+$/.test(name))
25
+ throw new Error('Invalid function slug');
26
+ runSupabase(
27
+ 'supabase',
28
+ [
29
+ 'functions',
30
+ 'download',
31
+ name,
32
+ '--project-ref',
33
+ projectId,
34
+ '--workdir',
35
+ root,
36
+ ],
37
+ {
38
+ cwd: root,
39
+ env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken },
40
+ stdio: 'pipe',
41
+ timeout: 180000,
42
+ windowsHide: true,
43
+ },
44
+ );
45
+ const destination = path.join(backupDir, 'edge-functions', name);
46
+ await copyDirectoryRecursive(
47
+ path.join(root, 'supabase', 'functions', name),
48
+ destination,
49
+ );
50
+ downloaded.push({
51
+ name,
52
+ slug: name,
53
+ version: f.version,
54
+ verify_jwt: f.verify_jwt,
55
+ files: await fs.readdir(destination),
56
+ });
57
+ }
58
+ return {
59
+ success: true,
60
+ reason: 'success',
61
+ functions: downloaded,
62
+ functions_count: downloaded.length,
63
+ success_count: downloaded.length,
64
+ error_count: 0,
65
+ method: 'cli-isolated',
66
+ };
67
+ } catch {
68
+ return {
69
+ success: false,
70
+ reason: 'function_download_failed',
71
+ functions: [],
72
+ };
73
+ } finally {
74
+ await fs.rm(root, { recursive: true, force: true });
75
+ }
76
+ };