sqlite-hub 0.3.2 → 0.4.0

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.
Files changed (63) hide show
  1. package/README.md +5 -5
  2. package/changelog.md +7 -0
  3. package/{js → frontend/js}/app.js +75 -3
  4. package/{js → frontend/js}/components/connectionCard.js +8 -9
  5. package/frontend/js/components/connectionLogo.js +33 -0
  6. package/{js → frontend/js}/components/emptyState.js +25 -11
  7. package/{js → frontend/js}/components/modal.js +57 -0
  8. package/{js → frontend/js}/components/sidebar.js +8 -3
  9. package/{js → frontend/js}/store.js +32 -0
  10. package/{js → frontend/js}/views/data.js +102 -9
  11. package/{js → frontend/js}/views/structure.js +10 -12
  12. package/{styles → frontend/styles}/structure-graph.css +5 -10
  13. package/package.json +2 -2
  14. package/{publish_brew.sh → scripts/publish_brew.sh} +2 -2
  15. package/{publish_npm.sh → scripts/publish_npm.sh} +2 -2
  16. package/server/data/db_logos/.gitkeep +0 -0
  17. package/server/routes/connections.js +2 -0
  18. package/server/server.js +7 -5
  19. package/server/services/sqlite/connectionManager.js +68 -33
  20. package/server/services/storage/appStateStore.js +159 -20
  21. package/server/utils/appPaths.js +42 -18
  22. /package/{assets → frontend/assets}/images/logo.webp +0 -0
  23. /package/{assets → frontend/assets}/images/logo_extrasmall.webp +0 -0
  24. /package/{assets → frontend/assets}/images/logo_raw.png +0 -0
  25. /package/{assets → frontend/assets}/images/logo_small.webp +0 -0
  26. /package/{assets → frontend/assets}/mockups/connections.png +0 -0
  27. /package/{assets → frontend/assets}/mockups/data.png +0 -0
  28. /package/{assets → frontend/assets}/mockups/data_edit.png +0 -0
  29. /package/{assets → frontend/assets}/mockups/graph_visualize.png +0 -0
  30. /package/{assets → frontend/assets}/mockups/home.png +0 -0
  31. /package/{assets → frontend/assets}/mockups/overview.png +0 -0
  32. /package/{assets → frontend/assets}/mockups/sql_editor.png +0 -0
  33. /package/{assets → frontend/assets}/mockups/structure.png +0 -0
  34. /package/{index.html → frontend/index.html} +0 -0
  35. /package/{js → frontend/js}/api.js +0 -0
  36. /package/{js → frontend/js}/components/actionBar.js +0 -0
  37. /package/{js → frontend/js}/components/appShell.js +0 -0
  38. /package/{js → frontend/js}/components/badges.js +0 -0
  39. /package/{js → frontend/js}/components/bottomTabs.js +0 -0
  40. /package/{js → frontend/js}/components/dataGrid.js +0 -0
  41. /package/{js → frontend/js}/components/metricCard.js +0 -0
  42. /package/{js → frontend/js}/components/pageHeader.js +0 -0
  43. /package/{js → frontend/js}/components/queryEditor.js +0 -0
  44. /package/{js → frontend/js}/components/queryResults.js +0 -0
  45. /package/{js → frontend/js}/components/rowEditorPanel.js +0 -0
  46. /package/{js → frontend/js}/components/statusBar.js +0 -0
  47. /package/{js → frontend/js}/components/structureGraph.js +0 -0
  48. /package/{js → frontend/js}/components/toast.js +0 -0
  49. /package/{js → frontend/js}/components/topNav.js +0 -0
  50. /package/{js → frontend/js}/lib/cytoscapeRuntime.js +0 -0
  51. /package/{js → frontend/js}/router.js +0 -0
  52. /package/{js → frontend/js}/utils/format.js +0 -0
  53. /package/{js → frontend/js}/views/connections.js +0 -0
  54. /package/{js → frontend/js}/views/editor.js +0 -0
  55. /package/{js → frontend/js}/views/landing.js +0 -0
  56. /package/{js → frontend/js}/views/overview.js +0 -0
  57. /package/{js → frontend/js}/views/settings.js +0 -0
  58. /package/{styles → frontend/styles}/base.css +0 -0
  59. /package/{styles → frontend/styles}/components.css +0 -0
  60. /package/{styles → frontend/styles}/layout.css +0 -0
  61. /package/{styles → frontend/styles}/tokens.css +0 -0
  62. /package/{styles → frontend/styles}/views.css +0 -0
  63. /package/{data → server/data}/.gitkeep +0 -0
@@ -6,8 +6,8 @@ function renderEntryGroup(title, entries, activeName, options = {}) {
6
6
  const { compact = false, showMeta = true } = options;
7
7
 
8
8
  return `
9
- <section class="shell-section p-5">
10
- <div class="mb-4 text-[10px] font-bold uppercase tracking-[0.25em] text-primary-container">
9
+ <section class="shell-section flex flex-col p-5">
10
+ <div class="mb-4 shrink-0 text-[10px] font-bold uppercase tracking-[0.25em] text-primary-container">
11
11
  ${escapeHtml(title)}
12
12
  </div>
13
13
  ${
@@ -209,10 +209,6 @@ function renderGraphSurface(structure, selectedName, detail, detailLoading) {
209
209
  type="search"
210
210
  />
211
211
  </label>
212
- <div class="structure-graph__toolbar-meta">
213
- ${escapeHtml(formatNumber(graph.tables?.length ?? 0))} TABLES //
214
- ${escapeHtml(formatNumber(graph.relationshipCount ?? 0))} RELATIONSHIPS
215
- </div>
216
212
  </div>
217
213
  <div class="structure-graph__toolbar-actions">
218
214
  <button
@@ -293,8 +289,8 @@ export function renderStructureView(state) {
293
289
 
294
290
  return {
295
291
  main: `
296
- <section class="view-surface min-h-full bg-surface-container">
297
- <div class="view-frame space-y-8">
292
+ <section class="view-surface flex h-full min-h-0 flex-col bg-surface-container">
293
+ <div class="view-frame flex h-full min-h-0 flex-col">
298
294
  ${renderPageHeader({
299
295
  title: "Structure",
300
296
  subtitle: "Schema graph, foreign-key paths, raw DDL, and object metadata",
@@ -303,7 +299,7 @@ export function renderStructureView(state) {
303
299
  ${
304
300
  state.structure.loading && !structure
305
301
  ? `
306
- <div class="flex min-h-[280px] items-center justify-center border border-outline-variant/10 bg-surface-container-low">
302
+ <div class="flex min-h-0 flex-1 items-center justify-center border border-outline-variant/10 bg-surface-container-low">
307
303
  <div class="text-center text-on-surface-variant/40">
308
304
  <span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
309
305
  <p class="font-mono text-[10px] uppercase tracking-[0.22em]">LOADING_STRUCTURE</p>
@@ -312,7 +308,7 @@ export function renderStructureView(state) {
312
308
  `
313
309
  : state.structure.error
314
310
  ? `
315
- <div class="border border-error/20 bg-error-container/10 px-6 py-5 text-sm text-on-surface">
311
+ <div class="min-h-0 flex-1 border border-error/20 bg-error-container/10 px-6 py-5 text-sm text-on-surface">
316
312
  <div class="font-headline text-xs font-bold uppercase tracking-[0.18em] text-error">
317
313
  ${escapeHtml(state.structure.error.code)}
318
314
  </div>
@@ -321,8 +317,9 @@ export function renderStructureView(state) {
321
317
  `
322
318
  : structure
323
319
  ? `
324
- <section class="grid grid-cols-1 gap-6 xl:grid-cols-[18.5rem_minmax(0,1fr)] 2xl:grid-cols-[19.5rem_minmax(0,1fr)]">
325
- <div class="space-y-6">
320
+ <section class="grid min-h-0 flex-1 grid-cols-1 gap-6 xl:grid-cols-[18.5rem_minmax(0,1fr)] 2xl:grid-cols-[19.5rem_minmax(0,1fr)]">
321
+ <div class="custom-scrollbar min-h-0 overflow-y-auto pr-1">
322
+ <div class="space-y-6">
326
323
  ${renderEntryGroup(
327
324
  "Tables",
328
325
  structure.grouped.tables,
@@ -345,6 +342,7 @@ export function renderStructureView(state) {
345
342
  structure.grouped.triggers,
346
343
  state.structure.selectedName
347
344
  )}
345
+ </div>
348
346
  </div>
349
347
  ${renderGraphSurface(
350
348
  structure,
@@ -3,7 +3,8 @@
3
3
  border: 1px solid rgba(138, 123, 52, 0.26);
4
4
  display: flex;
5
5
  flex-direction: column;
6
- min-height: 760px;
6
+ height: 100%;
7
+ min-height: 0;
7
8
  overflow: hidden;
8
9
  }
9
10
 
@@ -100,14 +101,6 @@
100
101
  appearance: none;
101
102
  }
102
103
 
103
- .structure-graph__toolbar-meta {
104
- color: rgba(231, 223, 189, 0.64);
105
- font-family: var(--font-family-mono);
106
- font-size: 0.625rem;
107
- letter-spacing: 0.18em;
108
- text-transform: uppercase;
109
- }
110
-
111
104
  .structure-graph__button {
112
105
  align-items: center;
113
106
  background: rgba(17, 17, 15, 0.94);
@@ -193,8 +186,10 @@
193
186
  .structure-graph__inspector {
194
187
  background: linear-gradient(to bottom, rgba(26, 25, 23, 0.98), rgba(11, 11, 10, 0.99));
195
188
  border-left: 1px solid rgba(138, 123, 52, 0.24);
189
+ height: 100%;
196
190
  min-height: 0;
197
- overflow: auto;
191
+ overflow-y: auto;
192
+ overflow-x: hidden;
198
193
  }
199
194
 
200
195
  .structure-graph.is-inspector-hidden .structure-graph__inspector {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlite-hub",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "SQLite-only local management app backend and SPA shell",
5
5
  "main": "server/server.js",
6
6
  "bin": {
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "start": "node bin/sqlite-hub.js",
11
11
  "dev": "node --watch server/server.js --port:4180",
12
- "publish": "bash publish_brew.sh && bash publish_npm.sh"
12
+ "publish": "bash scripts/publish_brew.sh && sleep 10 && npm login --auth-type=web && bash scripts/publish_npm.sh"
13
13
  },
14
14
  "dependencies": {
15
15
  "better-sqlite3": "^11.8.1",
@@ -2,7 +2,7 @@
2
2
 
3
3
  set -euo pipefail
4
4
 
5
- ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
6
  cd "$ROOT_DIR"
7
7
 
8
8
  DRY_RUN=0
@@ -17,7 +17,7 @@ GH_REMOTE="origin"
17
17
  usage() {
18
18
  cat <<'EOF'
19
19
  Usage:
20
- ./publish.sh [options]
20
+ ./scripts/publish_brew.sh [options]
21
21
 
22
22
  Options:
23
23
  --version X.Y.Z Override the package.json version for this publish run.
@@ -2,7 +2,7 @@
2
2
 
3
3
  set -euo pipefail
4
4
 
5
- ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
6
  cd "$ROOT_DIR"
7
7
 
8
8
  DRY_RUN=0
@@ -15,7 +15,7 @@ GH_REMOTE="origin"
15
15
  usage() {
16
16
  cat <<'EOF'
17
17
  Usage:
18
- ./publish_npm.sh [options]
18
+ ./scripts/publish_npm.sh [options]
19
19
 
20
20
  Options:
21
21
  --tag NAME Publish to a custom npm dist-tag instead of `latest`.
File without changes
@@ -113,6 +113,8 @@ function createConnectionsRouter({ connectionManager, importService, backupServi
113
113
  filePath: req.body.path,
114
114
  label: req.body.label,
115
115
  readOnly: Boolean(req.body.readOnly),
116
+ logoUpload: req.body.logoUpload ?? null,
117
+ clearLogo: Boolean(req.body.clearLogo),
116
118
  });
117
119
 
118
120
  res.json(
package/server/server.js CHANGED
@@ -20,7 +20,10 @@ const { createSettingsRouter } = require("./routes/settings");
20
20
  const { createExportRouter } = require("./routes/export");
21
21
 
22
22
  const PACKAGE_ROOT = path.resolve(__dirname, "..");
23
+ const FRONTEND_ROOT = path.join(PACKAGE_ROOT, "frontend");
24
+ const FRONTEND_ENTRYPOINT = path.join(FRONTEND_ROOT, "index.html");
23
25
  const {
26
+ appStateDirectory: APP_STATE_DIRECTORY,
24
27
  appStateDbPath: APP_STATE_DB_PATH,
25
28
  legacyStatePath: LEGACY_STATE_PATH,
26
29
  legacyDatabasePaths: LEGACY_DATABASE_PATHS,
@@ -83,11 +86,11 @@ app.get("/favicon.ico", (req, res) => {
83
86
  });
84
87
 
85
88
  app.get("/", (req, res) => {
86
- res.sendFile(path.resolve(__dirname, "..", "index.html"));
89
+ res.sendFile(FRONTEND_ENTRYPOINT);
87
90
  });
88
91
 
89
92
  app.get("/index.html", (req, res) => {
90
- res.sendFile(path.resolve(__dirname, "..", "index.html"));
93
+ res.sendFile(FRONTEND_ENTRYPOINT);
91
94
  });
92
95
 
93
96
  app.use(
@@ -102,9 +105,8 @@ app.use(
102
105
  "/vendor/elkjs",
103
106
  express.static(path.resolve(__dirname, "..", "node_modules", "elkjs"))
104
107
  );
105
- app.use("/js", express.static(path.resolve(__dirname, "..", "js")));
106
- app.use("/styles", express.static(path.resolve(__dirname, "..", "styles")));
107
- app.use("/assets", express.static(path.resolve(__dirname, "..", "assets")));
108
+ app.use(express.static(FRONTEND_ROOT));
109
+ app.use("/db_logos", express.static(path.join(APP_STATE_DIRECTORY, "db_logos")));
108
110
  app.use(errorMiddleware);
109
111
 
110
112
  function parsePortArgument(argv = process.argv.slice(2)) {
@@ -44,6 +44,7 @@ class ConnectionManager {
44
44
  filePath: recent.path,
45
45
  label: recent.label,
46
46
  id: recent.id,
47
+ logoPath: recent.logoPath ?? null,
47
48
  makeActive: true,
48
49
  });
49
50
  } catch (error) {
@@ -54,17 +55,26 @@ class ConnectionManager {
54
55
 
55
56
  buildConnectionRecord(filePath, options = {}) {
56
57
  const metadata = getFileMetadata(filePath);
58
+ const id =
59
+ options.id ??
60
+ `conn_${crypto.createHash("sha1").update(filePath).digest("hex").slice(0, 16)}`;
61
+ const existingConnection = this.appStateStore
62
+ .getRecentConnections()
63
+ .find((connection) => connection.id === id);
64
+ const logoPath = Object.prototype.hasOwnProperty.call(options, "logoPath")
65
+ ? options.logoPath
66
+ : (existingConnection?.logoPath ?? null);
57
67
 
58
68
  return {
59
- id:
60
- options.id ??
61
- `conn_${crypto.createHash("sha1").update(filePath).digest("hex").slice(0, 16)}`,
69
+ id,
62
70
  label: options.label?.trim() || path.basename(filePath),
63
71
  path: filePath,
64
72
  lastOpenedAt: new Date().toISOString(),
65
73
  lastModifiedAt: metadata.lastModifiedAt,
66
74
  sizeBytes: metadata.sizeBytes,
67
75
  readOnly: options.readOnly ?? !isWritable(filePath),
76
+ logoPath,
77
+ logoUrl: this.appStateStore.getConnectionLogoUrl(logoPath),
68
78
  };
69
79
  }
70
80
 
@@ -94,7 +104,7 @@ class ConnectionManager {
94
104
  this.current = null;
95
105
  }
96
106
 
97
- openConnection({ filePath, label, id, makeActive = true, readOnly = false }) {
107
+ openConnection({ filePath, label, id, makeActive = true, readOnly = false, logoPath }) {
98
108
  const resolvedPath = validateSqlitePath(filePath, { mustExist: true });
99
109
  const db = this.openRawDatabase(resolvedPath, {
100
110
  fileMustExist: true,
@@ -103,11 +113,17 @@ class ConnectionManager {
103
113
 
104
114
  this.closeCurrent();
105
115
 
106
- const connection = this.buildConnectionRecord(resolvedPath, {
116
+ const connectionOptions = {
107
117
  id,
108
118
  label,
109
119
  readOnly,
110
- });
120
+ };
121
+
122
+ if (logoPath !== undefined) {
123
+ connectionOptions.logoPath = logoPath;
124
+ }
125
+
126
+ const connection = this.buildConnectionRecord(resolvedPath, connectionOptions);
111
127
 
112
128
  this.current = {
113
129
  ...connection,
@@ -186,6 +202,7 @@ class ConnectionManager {
186
202
  id: recent.id,
187
203
  makeActive: true,
188
204
  readOnly: recent.readOnly,
205
+ logoPath: recent.logoPath ?? null,
189
206
  });
190
207
  }
191
208
 
@@ -199,7 +216,7 @@ class ConnectionManager {
199
216
  return state.recentConnections;
200
217
  }
201
218
 
202
- updateRecentConnection(id, { filePath, label, readOnly = false }) {
219
+ updateRecentConnection(id, { filePath, label, readOnly = false, logoUpload = null, clearLogo = false }) {
203
220
  const recentConnections = this.appStateStore.getRecentConnections();
204
221
  const existing = recentConnections.find((connection) => connection.id === id);
205
222
 
@@ -218,40 +235,58 @@ class ConnectionManager {
218
235
 
219
236
  const normalizedLabel = label?.trim() || path.basename(resolvedPath);
220
237
  const normalizedReadOnly = Boolean(readOnly);
238
+ let nextLogoPath = clearLogo ? null : existing.logoPath ?? null;
239
+ let createdLogoPath = null;
221
240
 
222
- if (this.current?.id === id) {
223
- return this.openConnection({
224
- filePath: resolvedPath,
225
- label: normalizedLabel,
226
- id,
227
- makeActive: true,
241
+ if (logoUpload) {
242
+ createdLogoPath = this.appStateStore.saveConnectionLogo(id, logoUpload);
243
+ nextLogoPath = createdLogoPath;
244
+ }
245
+
246
+ try {
247
+ if (this.current?.id === id) {
248
+ return this.openConnection({
249
+ filePath: resolvedPath,
250
+ label: normalizedLabel,
251
+ id,
252
+ makeActive: true,
253
+ readOnly: normalizedReadOnly,
254
+ logoPath: nextLogoPath,
255
+ });
256
+ }
257
+
258
+ const db = this.openRawDatabase(resolvedPath, {
259
+ fileMustExist: true,
228
260
  readOnly: normalizedReadOnly,
229
261
  });
230
- }
231
262
 
232
- const db = this.openRawDatabase(resolvedPath, {
233
- fileMustExist: true,
234
- readOnly: normalizedReadOnly,
235
- });
263
+ db.close();
236
264
 
237
- db.close();
265
+ const metadata = getFileMetadata(resolvedPath);
266
+ const nextConnection = {
267
+ ...existing,
268
+ label: normalizedLabel,
269
+ path: resolvedPath,
270
+ lastModifiedAt: metadata.lastModifiedAt,
271
+ sizeBytes: metadata.sizeBytes,
272
+ readOnly: normalizedReadOnly,
273
+ logoPath: nextLogoPath,
274
+ };
238
275
 
239
- const metadata = getFileMetadata(resolvedPath);
240
- const nextConnection = {
241
- ...existing,
242
- label: normalizedLabel,
243
- path: resolvedPath,
244
- lastModifiedAt: metadata.lastModifiedAt,
245
- sizeBytes: metadata.sizeBytes,
246
- readOnly: normalizedReadOnly,
247
- };
276
+ this.appStateStore.updateRecentConnection(id, () => nextConnection);
248
277
 
249
- this.appStateStore.updateRecentConnection(id, () => nextConnection);
278
+ return {
279
+ ...nextConnection,
280
+ logoUrl: this.appStateStore.getConnectionLogoUrl(nextLogoPath),
281
+ isActive: false,
282
+ };
283
+ } catch (error) {
284
+ if (createdLogoPath) {
285
+ this.appStateStore.deleteConnectionLogo(createdLogoPath);
286
+ }
250
287
 
251
- return {
252
- ...nextConnection,
253
- isActive: false,
254
- };
288
+ throw error;
289
+ }
255
290
  }
256
291
 
257
292
  getActiveConnection() {