sqlite-hub 0.2.0 → 0.3.1

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.
@@ -0,0 +1,2 @@
1
+ github: oliverjessner
2
+ buy_me_a_coffee: oliverjessner
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 oliverjessner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,48 +1,74 @@
1
- # sqlite-hub
2
-
3
- A cyberpunk inspired management app for sqlite
1
+ # sqlite-hub ⚡️
4
2
 
5
3
  ![](/assets/mockups/home.png)
6
4
 
7
- ## Install
5
+ A focused local-first app for browsing, editing, and querying SQLite databases.
8
6
 
9
- ### Homebrew
7
+ SQLite Hub is built for developers and technical users who want a clean SQLite workflow without heavy database clients, cloud layers, or dashboard noise.
10
8
 
11
- ```bash
12
- brew install oliverjessner/tap/sqlite-hub
13
- ```
9
+ ## Why SQLite Hub?
14
10
 
15
- Or tap first and then install:
11
+ Many database tools are powerful, but feel oversized when all you want is to inspect a local SQLite file, edit a few rows, and run a query fast.
16
12
 
17
- ```bash
18
- brew tap oliverjessner/tap
19
- brew install sqlite-hub
20
- ```
13
+ SQLite Hub keeps that workflow sharp:
21
14
 
22
- ### NPM
15
+ - browse tables and rows
16
+ - inspect schema and structure
17
+ - edit records in place
18
+ - export tables and query results as CSV
19
+ - create simple local backups of the active database
20
+ - run SQL in a syntax-highlighted editor
21
+ - stay local and move fast
23
22
 
24
- ```bash
25
- npm install sqlite-hub
26
- ```
23
+ ## Features
27
24
 
28
- ## Run
25
+ ### Structure view
29
26
 
30
- Start the app and open it automatically in the default browser:
27
+ ![](/assets/mockups/graph_visualize.png)
31
28
 
32
- ```bash
33
- npm start
34
- ```
29
+ Inspect tables, columns, types, and schema details without losing pace. Visualized in a graph.
30
+
31
+ ### Data browser
32
+
33
+ ![](/assets/mockups/data.png)
34
+
35
+ Scan rows, sort fast, move through local data quickly, and export full tables as CSV.
35
36
 
36
- Use a custom port via CLI. If omitted, `4173` is used:
37
+ ### Row editing
38
+
39
+ ![](/assets/mockups/data_edit.png)
40
+
41
+ Open one record, edit it in place, commit, continue.
42
+
43
+ ### SQL editor
44
+
45
+ ![](/assets/mockups/sql_editor.png)
46
+
47
+ Write queries in a syntax-highlighted editor, inspect results in the same workflow, and export result sets as CSV.
48
+
49
+ ### Simple backups
50
+
51
+ Create timestamped local backups of the active SQLite database in one click. Backups are stored as plain file copies in a local `backups` folder next to the database.
52
+
53
+ ### Local-first
54
+
55
+ Built around local SQLite files, not hosted dashboards or team complexity.
56
+
57
+ ## Install
58
+
59
+ ### Homebrew
37
60
 
38
61
  ```bash
39
- npm start -- --port:1203
62
+ brew tap oliverjessner/tap
63
+ brew install sqlite-hub
40
64
  ```
41
65
 
42
- After linking or installing globally, the binary can also be called directly:
66
+ ### NPM
43
67
 
44
68
  ```bash
45
- sqlite-hub --port:1203
69
+ npm install -g sqlit-hub
46
70
  ```
47
71
 
48
- App state such as recent connections, SQL history, and local settings is stored in the user profile instead of the install directory. On macOS this lives under `~/Library/Application Support/sqlite-hub/`, so Homebrew upgrades keep the internal state across versions.
72
+ ## Changelog
73
+
74
+ [Changelog](changelog.md)
package/changelog.md CHANGED
@@ -1,7 +1,13 @@
1
+ # v0.3.1
2
+
3
+ - visualize tables
4
+ - export csv from data and sql_editor
5
+ - Backup mode
6
+
1
7
  # v0.2.0
2
8
 
3
9
  - db fix
4
10
 
5
11
  # v0.1.3
6
12
 
7
- - edit in sql editro
13
+ - edit in sql editor
package/index.html CHANGED
@@ -90,11 +90,14 @@
90
90
  <link href="styles/layout.css" rel="stylesheet" />
91
91
  <link href="styles/components.css" rel="stylesheet" />
92
92
  <link href="styles/views.css" rel="stylesheet" />
93
+ <link href="styles/structure-graph.css" rel="stylesheet" />
93
94
  </head>
94
95
  <body
95
96
  class="bg-background text-on-surface font-body selection:bg-primary-container selection:text-on-primary overflow-hidden"
96
97
  >
97
98
  <div id="app"></div>
99
+ <script src="/vendor/elkjs/lib/elk.bundled.js"></script>
100
+ <script src="/vendor/cytoscape-elk/dist/cytoscape-elk.js"></script>
98
101
  <script src="js/app.js" type="module"></script>
99
102
  </body>
100
103
  </html>
package/js/api.js CHANGED
@@ -113,6 +113,12 @@ export function updateRecentConnection(id, payload) {
113
113
  });
114
114
  }
115
115
 
116
+ export function createActiveConnectionBackup() {
117
+ return request("/api/connections/backup-active", {
118
+ method: "POST",
119
+ });
120
+ }
121
+
116
122
  export function getOverview() {
117
123
  return request("/api/db/overview");
118
124
  }
@@ -191,3 +197,11 @@ export function downloadQueryCsv(sql) {
191
197
  fallbackFilename: "query-results.csv",
192
198
  });
193
199
  }
200
+
201
+ export function downloadTableCsv(tableName) {
202
+ return download("/api/export/table.csv", {
203
+ method: "POST",
204
+ body: { tableName },
205
+ fallbackFilename: `${tableName || "table"}.csv`,
206
+ });
207
+ }
package/js/app.js CHANGED
@@ -2,10 +2,16 @@ import { renderAppShell } from "./components/appShell.js";
2
2
  import { renderModal } from "./components/modal.js";
3
3
  import { renderSidebar } from "./components/sidebar.js";
4
4
  import { renderStatusBar } from "./components/statusBar.js";
5
+ import {
6
+ mountStructureGraph,
7
+ resetPersistedStructureGraphState,
8
+ teardownStructureGraph,
9
+ } from "./components/structureGraph.js";
5
10
  import { renderToasts } from "./components/toast.js";
6
11
  import { renderTopNav } from "./components/topNav.js";
7
12
  import { createRouter } from "./router.js";
8
13
  import {
14
+ createActiveConnectionBackup,
9
15
  clearCurrentQuery,
10
16
  clearDataRowSelection,
11
17
  clearEditorRowSelection,
@@ -14,6 +20,7 @@ import {
14
20
  closeModal,
15
21
  dismissToast,
16
22
  executeCurrentQuery,
23
+ exportCurrentDataTableCsv,
17
24
  exportCurrentQueryCsv,
18
25
  getState,
19
26
  initializeApp,
@@ -63,6 +70,10 @@ const shellRefs = {
63
70
  toast: document.querySelector("#toast-root"),
64
71
  };
65
72
 
73
+ function resetStructureGraphForDatabaseChange() {
74
+ resetPersistedStructureGraphState();
75
+ }
76
+
66
77
  function renderQueryHighlightMarkup(query) {
67
78
  if (query) {
68
79
  return highlightSql(query);
@@ -212,6 +223,7 @@ function renderApp(state) {
212
223
  const { main, panel } = resolveView(state);
213
224
  const isLockedRoute = ["editor", "editorResults", "data"].includes(state.route.name);
214
225
 
226
+ teardownStructureGraph();
215
227
  shellRefs.topNav.innerHTML = renderTopNav(state);
216
228
  shellRefs.sidebar.innerHTML = renderSidebar(state);
217
229
  shellRefs.statusBar.innerHTML = renderStatusBar(state);
@@ -222,6 +234,12 @@ function renderApp(state) {
222
234
  shellRefs.toast.innerHTML = renderToasts(state.toasts);
223
235
  shellRefs.shell.classList.toggle("panel-open", Boolean(panel));
224
236
  restoreFocusedInputState(focusedInput);
237
+
238
+ if (state.route.name === "structure") {
239
+ mountStructureGraph(state).catch((error) => {
240
+ console.error("Failed to mount structure graph.", error);
241
+ });
242
+ }
225
243
  }
226
244
 
227
245
  const router = createRouter((route) => {
@@ -251,6 +269,7 @@ async function handleAction(actionNode) {
251
269
  dismissToast(actionNode.dataset.toastId);
252
270
  return;
253
271
  case "select-connection": {
272
+ resetStructureGraphForDatabaseChange();
254
273
  const next = await selectConnection(actionNode.dataset.connectionId);
255
274
  if (next) {
256
275
  router.navigate("/overview");
@@ -258,6 +277,12 @@ async function handleAction(actionNode) {
258
277
  return;
259
278
  }
260
279
  case "remove-connection": {
280
+ const isActiveConnection = getState().connections.active?.id === actionNode.dataset.connectionId;
281
+
282
+ if (isActiveConnection) {
283
+ resetStructureGraphForDatabaseChange();
284
+ }
285
+
261
286
  const removed = await removeConnection(actionNode.dataset.connectionId);
262
287
  if (removed) {
263
288
  const nextState = getState();
@@ -269,6 +294,9 @@ async function handleAction(actionNode) {
269
294
  }
270
295
  return;
271
296
  }
297
+ case "create-backup":
298
+ await createActiveConnectionBackup();
299
+ return;
272
300
  case "execute-query": {
273
301
  const success = await executeCurrentQuery();
274
302
  router.navigate(success ? "/editor/results" : "/editor");
@@ -296,6 +324,9 @@ async function handleAction(actionNode) {
296
324
  case "export-query-csv":
297
325
  await exportCurrentQueryCsv();
298
326
  return;
327
+ case "export-data-csv":
328
+ await exportCurrentDataTableCsv();
329
+ return;
299
330
  case "select-structure-entry":
300
331
  if (actionNode.dataset.entryName) {
301
332
  await selectStructureEntry(actionNode.dataset.entryName);
@@ -422,6 +453,7 @@ document.addEventListener("submit", async (event) => {
422
453
 
423
454
  switch (form.dataset.form) {
424
455
  case "open-connection": {
456
+ resetStructureGraphForDatabaseChange();
425
457
  const connection = await submitOpenConnection({
426
458
  path: String(formData.get("path") ?? ""),
427
459
  label: String(formData.get("label") ?? ""),
@@ -434,6 +466,7 @@ document.addEventListener("submit", async (event) => {
434
466
  return;
435
467
  }
436
468
  case "create-connection": {
469
+ resetStructureGraphForDatabaseChange();
437
470
  const connection = await submitCreateConnection({
438
471
  path: String(formData.get("path") ?? ""),
439
472
  label: String(formData.get("label") ?? ""),
@@ -445,6 +478,7 @@ document.addEventListener("submit", async (event) => {
445
478
  return;
446
479
  }
447
480
  case "import-sql": {
481
+ resetStructureGraphForDatabaseChange();
448
482
  const targetMode = String(formData.get("targetMode") ?? "active");
449
483
  const payload = {
450
484
  sqlFilePath: String(formData.get("sqlFilePath") ?? ""),
@@ -467,7 +501,14 @@ document.addEventListener("submit", async (event) => {
467
501
  return;
468
502
  }
469
503
  case "edit-connection": {
470
- await submitEditConnection(String(formData.get("connectionId") ?? ""), {
504
+ const connectionId = String(formData.get("connectionId") ?? "");
505
+ const isActiveConnection = getState().connections.active?.id === connectionId;
506
+
507
+ if (isActiveConnection) {
508
+ resetStructureGraphForDatabaseChange();
509
+ }
510
+
511
+ await submitEditConnection(connectionId, {
471
512
  path: String(formData.get("path") ?? ""),
472
513
  label: String(formData.get("label") ?? ""),
473
514
  readOnly: formData.get("readOnly") === "on",
@@ -38,6 +38,7 @@ export function renderQueryEditor({
38
38
  query,
39
39
  title,
40
40
  executing = false,
41
+ exporting = false,
41
42
  history = [],
42
43
  historyLoading = false,
43
44
  }) {
@@ -75,6 +76,13 @@ export function renderQueryEditor({
75
76
  >
76
77
  Clear
77
78
  </button>
79
+ <button
80
+ class="px-4 py-1.5 text-[10px] font-bold uppercase tracking-widest text-on-surface hover:bg-surface-container-highest transition-colors"
81
+ data-action="export-query-csv"
82
+ type="button"
83
+ >
84
+ ${exporting ? "Exporting..." : "Export CSV"}
85
+ </button>
78
86
  <button
79
87
  class="bg-primary-container px-6 py-1.5 text-xs font-black uppercase tracking-tighter text-on-primary shadow-[0_0_15px_-5px_rgba(252,227,0,0.4)] transition-all hover:brightness-110"
80
88
  data-action="execute-query"