Skip to content

Commit 05c9a0b

Browse files
committed
feat: Implement database download functionality with a new API endpoint and UI option.
1 parent 0645421 commit 05c9a0b

File tree

5 files changed

+94
-35
lines changed

5 files changed

+94
-35
lines changed

demo/bun.lock

Lines changed: 13 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

demo/index.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,9 @@ console.log(" 📋 Tabelas: clientes, produtos, pedidos, categorias\n");
132132
const app = new Elysia()
133133
.use(
134134
rateLimit({
135-
exclude: ["/sqlite", "/sqlite/*"],
136-
})
135+
exclude: ["/sqlite", "/sqlite/api/db/download", "/sqlite/*"],
136+
skip: ({ url }) => url.includes("/api/db/download"),
137+
}),
137138
)
138139
// Página inicial com instruções
139140
.get("/", () => {
@@ -276,7 +277,7 @@ const app = new Elysia()
276277
</body>
277278
</html>
278279
`,
279-
{ headers: { "Content-Type": "text/html" } }
280+
{ headers: { "Content-Type": "text/html" } },
280281
);
281282
})
282283

@@ -292,5 +293,5 @@ console.log("💡 Dica: Experimente consultas SQL como:");
292293
console.log(" SELECT * FROM clientes");
293294
console.log(" SELECT * FROM produtos WHERE preco > 5000");
294295
console.log(
295-
" SELECT c.nome, pe.total FROM pedidos pe JOIN clientes c ON pe.cliente_id = c.id\n"
296+
" SELECT c.nome, pe.total FROM pedidos pe JOIN clientes c ON pe.cliente_id = c.id\n",
296297
);

demo/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
"dev": "bun run --watch index.js"
99
},
1010
"dependencies": {
11-
"elysia": "^1.2.25"
11+
"@sinclair/typebox": "^0.34.48",
12+
"elysia": "^1.2.25",
13+
"elysia-rate-limit": "^4.5.0"
1214
}
1315
}

src/index.js

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ export const sqliteAdmin = ({ dbPath, configPath }) => {
238238
const otpauth = authenticator.keyuri(
239239
session.username,
240240
"SQLite",
241-
secret
241+
secret,
242242
);
243243
const qrCode = await QRCode.toDataURL(otpauth);
244244
return { success: true, secret, qrCode };
@@ -327,7 +327,7 @@ export const sqliteAdmin = ({ dbPath, configPath }) => {
327327
SELECT name FROM sqlite_master
328328
WHERE type='table' AND name NOT LIKE 'sqlite_%'
329329
ORDER BY name
330-
`
330+
`,
331331
)
332332
.all();
333333
return { tables: tables.map((t) => t.name) };
@@ -342,7 +342,7 @@ export const sqliteAdmin = ({ dbPath, configPath }) => {
342342

343343
const rows = db
344344
.query(
345-
`SELECT * FROM ${params.name} LIMIT ${limit} OFFSET ${offset}`
345+
`SELECT * FROM ${params.name} LIMIT ${limit} OFFSET ${offset}`,
346346
)
347347
.all();
348348
const countResult = db
@@ -406,7 +406,7 @@ export const sqliteAdmin = ({ dbPath, configPath }) => {
406406
const placeholders = columns.map(() => "?").join(", ");
407407
const values = Object.values(body);
408408
const sql = `INSERT INTO ${params.name} (${columns.join(
409-
", "
409+
", ",
410410
)}) VALUES (${placeholders})`;
411411
const result = db.run(sql, values);
412412
return { success: true, id: result.lastInsertRowid };
@@ -481,7 +481,7 @@ export const sqliteAdmin = ({ dbPath, configPath }) => {
481481
const displayCol =
482482
tableInfo.find(
483483
(c) =>
484-
c.type?.toUpperCase().includes("TEXT") && c.name !== refColumn
484+
c.type?.toUpperCase().includes("TEXT") && c.name !== refColumn,
485485
)?.name || refColumn;
486486

487487
const sql = `SELECT ${refColumn} as value, ${displayCol} as label FROM ${refTable} ORDER BY ${displayCol}`;
@@ -512,7 +512,7 @@ export const sqliteAdmin = ({ dbPath, configPath }) => {
512512
const displayCol =
513513
tableInfo.find(
514514
(c) =>
515-
c.type?.toUpperCase().includes("TEXT") && c.name !== idColumn
515+
c.type?.toUpperCase().includes("TEXT") && c.name !== idColumn,
516516
)?.name || idColumn;
517517

518518
const placeholders = ids.map(() => "?").join(",");
@@ -551,7 +551,7 @@ export const sqliteAdmin = ({ dbPath, configPath }) => {
551551
// Get database schema context
552552
const tables = db
553553
.query(
554-
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
554+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
555555
)
556556
.all();
557557
let schemaContext = "";
@@ -584,7 +584,7 @@ export const sqliteAdmin = ({ dbPath, configPath }) => {
584584
},
585585
],
586586
}),
587-
}
587+
},
588588
);
589589

590590
const data = await response.json();
@@ -604,7 +604,7 @@ export const sqliteAdmin = ({ dbPath, configPath }) => {
604604
try {
605605
const tables = db
606606
.query(
607-
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
607+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
608608
)
609609
.all();
610610
const schema = tables.map((t) => {
@@ -625,5 +625,28 @@ export const sqliteAdmin = ({ dbPath, configPath }) => {
625625
return { success: false, error: error.message };
626626
}
627627
})
628+
629+
// Download database file
630+
.get("/api/db/download", () => {
631+
try {
632+
const file = Bun.file(absoluteDbPath);
633+
const filename =
634+
absoluteDbPath.split(/[/\\]/).pop() || "database.sqlite";
635+
return new Response(file, {
636+
headers: {
637+
"Content-Type": "application/octet-stream",
638+
"Content-Disposition": `attachment; filename="${filename}"`,
639+
},
640+
});
641+
} catch (error) {
642+
return new Response(
643+
JSON.stringify({ success: false, error: error.message }),
644+
{
645+
status: 500,
646+
headers: { "Content-Type": "application/json" },
647+
},
648+
);
649+
}
650+
})
628651
);
629652
};

src/ui/src/App.jsx

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ const NewRecordModal = ({
152152

153153
try {
154154
const res = await fetch(
155-
`${API}/table/${currentTable}/fk-options?refTable=${col.fk.table}&refColumn=${col.fk.column}`
155+
`${API}/table/${currentTable}/fk-options?refTable=${col.fk.table}&refColumn=${col.fk.column}`,
156156
);
157157
const data = await res.json();
158158

@@ -460,11 +460,11 @@ export default function App() {
460460
];
461461

462462
const filteredTables = tables.filter((t) =>
463-
t.name.toLowerCase().includes(commandQuery.toLowerCase())
463+
t.name.toLowerCase().includes(commandQuery.toLowerCase()),
464464
);
465465

466466
const filteredActions = commandActions.filter((a) =>
467-
a.label.toLowerCase().includes(commandQuery.toLowerCase())
467+
a.label.toLowerCase().includes(commandQuery.toLowerCase()),
468468
);
469469

470470
// Combined items for keyboard navigation
@@ -541,7 +541,7 @@ export default function App() {
541541
} catch {
542542
return { name, count: "?" };
543543
}
544-
})
544+
}),
545545
);
546546
setTables(tablesWithCount);
547547
}
@@ -604,7 +604,7 @@ export default function App() {
604604

605605
if (searchQuery) {
606606
const textCols = columns.filter((c) =>
607-
c.type?.toUpperCase().includes("TEXT")
607+
c.type?.toUpperCase().includes("TEXT"),
608608
);
609609
if (textCols.length > 0) {
610610
const searchConds = textCols
@@ -694,7 +694,7 @@ export default function App() {
694694
if (data.rows) {
695695
setRows(data.rows);
696696
setColumns(
697-
data.columns?.map((c) => ({ name: c, type: "TEXT" })) || []
697+
data.columns?.map((c) => ({ name: c, type: "TEXT" })) || [],
698698
);
699699
setTotal(data.rows.length);
700700
} else {
@@ -797,15 +797,15 @@ export default function App() {
797797
...exportRows.map((r) =>
798798
cols
799799
.map((c) => `"${String(r[c] || "").replace(/"/g, '""')}"`)
800-
.join(",")
800+
.join(","),
801801
),
802802
].join("\n");
803803
downloadFile(`${currentTable}.csv`, csv, "text/csv");
804804
} else {
805805
downloadFile(
806806
`${currentTable}.json`,
807807
JSON.stringify(exportRows, null, 2),
808-
"application/json"
808+
"application/json",
809809
);
810810
}
811811

@@ -861,6 +861,23 @@ export default function App() {
861861
setAiLoading(false);
862862
};
863863

864+
// Download the DB file
865+
const downloadDb = async () => {
866+
try {
867+
const dbUrl = `${API}/db/download`;
868+
const a = document.createElement("a");
869+
a.href = dbUrl;
870+
// Triggers standard browser download
871+
a.click();
872+
} catch (err) {
873+
notifications.show({
874+
title: "Erro",
875+
message: "Erro ao baixar banco de dados",
876+
color: "red",
877+
});
878+
}
879+
};
880+
864881
// Inline edit - update cell
865882
const updateCell = async (rowPk, column, newValue) => {
866883
const pkCol = columns.find((c) => c.pk === 1)?.name || columns[0]?.name;
@@ -884,8 +901,8 @@ export default function App() {
884901
prevRows.map((r) =>
885902
String(r[pkCol]) === String(rowPk)
886903
? { ...r, [column]: newValue }
887-
: r
888-
)
904+
: r,
905+
),
889906
);
890907
notifications.show({
891908
title: "Salvo",
@@ -984,7 +1001,7 @@ export default function App() {
9841001
const values = new Set(
9851002
rows
9861003
.map((r) => r[col.name])
987-
.filter((v) => v !== null && v !== undefined)
1004+
.filter((v) => v !== null && v !== undefined),
9881005
);
9891006
// Limit to 20 options to keep UI clean
9901007
options[col.name] = Array.from(values).map(String).slice(0, 20);
@@ -1072,6 +1089,12 @@ export default function App() {
10721089
onClick={() => loadErd()}
10731090
style={{ borderRadius: 6 }}
10741091
/>
1092+
<NavLink
1093+
label="Download .sqlite/.db"
1094+
leftSection={<IconDownload size={16} />}
1095+
onClick={() => downloadDb()}
1096+
style={{ borderRadius: 6 }}
1097+
/>
10751098
</Stack>
10761099

10771100
<Divider my="sm" color="#E8E5E0" />
@@ -1660,9 +1683,9 @@ export default function App() {
16601683
r[
16611684
columns.find((c) => c.pk === 1)?.name ||
16621685
columns[0]?.name
1663-
]
1664-
)
1665-
)
1686+
],
1687+
),
1688+
),
16661689
)
16671690
: rows
16681691
}
@@ -1732,9 +1755,9 @@ export default function App() {
17321755
r[
17331756
columns.find((c) => c.pk === 1)?.name ||
17341757
columns[0]?.name
1735-
]
1736-
)
1737-
)
1758+
],
1759+
),
1760+
),
17381761
)}
17391762
columns={columns}
17401763
filename={`${currentTable}_selected`}
@@ -1776,7 +1799,7 @@ export default function App() {
17761799
onSelectAll={(checked) => {
17771800
if (checked) {
17781801
setSelectedRows(
1779-
new Set(rows.map((r) => String(r[pk])))
1802+
new Set(rows.map((r) => String(r[pk]))),
17801803
);
17811804
} else {
17821805
setSelectedRows(new Set());

0 commit comments

Comments
 (0)