smoonb 1.0.6 → 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/package.json CHANGED
@@ -1,66 +1,68 @@
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.8",
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
+ "test:restore:docker": "node tests/integration/verified-restore.js"
23
+ },
24
+ "keywords": [
25
+ "supabase",
26
+ "backup",
27
+ "migration",
28
+ "postgresql",
29
+ "edge-functions",
30
+ "database",
31
+ "cli",
32
+ "tool"
33
+ ],
34
+ "author": "Goalmoon Tecnologia LTDA <https://www.smoonb.com>",
35
+ "license": "SEE LICENSE IN LICENSE.md",
36
+ "engines": {
37
+ "node": ">=20.0.0"
38
+ },
39
+ "dependencies": {
40
+ "@supabase/supabase-js": "^2.93.0",
41
+ "adm-zip": "^0.5.16",
42
+ "chalk": "^4.1.2",
43
+ "commander": "^11.1.0",
44
+ "env-paths": "^4.0.0",
45
+ "inquirer": "^8.2.7"
46
+ },
47
+ "type": "commonjs",
48
+ "devDependencies": {
49
+ "eslint": "^9.38.0"
50
+ },
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/almmello/smoonb.git"
54
+ },
55
+ "bugs": {
56
+ "url": "https://github.com/almmello/smoonb/issues"
57
+ },
58
+ "homepage": "https://www.smoonb.com",
59
+ "files": [
60
+ "bin/",
61
+ "src/",
62
+ "README.md",
63
+ "README.pt-BR.md",
64
+ "LICENSE",
65
+ "LICENSE.pt-BR.md",
66
+ "CHANGELOG.md"
67
+ ]
68
+ }
@@ -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;
@@ -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,