sqlite-hub 0.3.2 → 0.5.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 (75) hide show
  1. package/README.md +5 -5
  2. package/changelog.md +14 -0
  3. package/{js → frontend/js}/api.js +99 -5
  4. package/{js → frontend/js}/app.js +162 -11
  5. package/{js → frontend/js}/components/connectionCard.js +8 -9
  6. package/frontend/js/components/connectionLogo.js +33 -0
  7. package/{js → frontend/js}/components/dataGrid.js +3 -3
  8. package/{js → frontend/js}/components/emptyState.js +25 -11
  9. package/{js → frontend/js}/components/modal.js +57 -0
  10. package/{js → frontend/js}/components/queryEditor.js +33 -55
  11. package/frontend/js/components/queryHistoryDetail.js +263 -0
  12. package/frontend/js/components/queryHistoryPanel.js +228 -0
  13. package/{js → frontend/js}/components/queryResults.js +32 -46
  14. package/{js → frontend/js}/components/rowEditorPanel.js +73 -14
  15. package/{js → frontend/js}/components/sidebar.js +8 -3
  16. package/{js → frontend/js}/store.js +577 -21
  17. package/{js → frontend/js}/utils/format.js +23 -0
  18. package/{js → frontend/js}/views/data.js +136 -10
  19. package/{js → frontend/js}/views/editor.js +34 -10
  20. package/{js → frontend/js}/views/overview.js +15 -0
  21. package/{js → frontend/js}/views/structure.js +10 -12
  22. package/{styles → frontend/styles}/components.css +106 -0
  23. package/{styles → frontend/styles}/structure-graph.css +5 -10
  24. package/package.json +2 -2
  25. package/{publish_brew.sh → scripts/publish_brew.sh} +2 -2
  26. package/{publish_npm.sh → scripts/publish_npm.sh} +2 -2
  27. package/server/data/db_logos/.gitkeep +0 -0
  28. package/server/routes/connections.js +2 -0
  29. package/server/routes/data.js +2 -0
  30. package/server/routes/export.js +4 -1
  31. package/server/routes/overview.js +12 -0
  32. package/server/routes/sql.js +163 -5
  33. package/server/server.js +8 -6
  34. package/server/services/sqlite/connectionManager.js +68 -33
  35. package/server/services/sqlite/dataBrowserService.js +4 -16
  36. package/server/services/sqlite/exportService.js +4 -16
  37. package/server/services/sqlite/overviewService.js +34 -0
  38. package/server/services/sqlite/sqlExecutor.js +83 -63
  39. package/server/services/sqlite/tableSort.js +63 -0
  40. package/server/services/storage/appStateStore.js +832 -20
  41. package/server/services/storage/queryHistoryUtils.js +169 -0
  42. package/server/utils/appPaths.js +42 -18
  43. package/{assets → frontend/assets}/images/logo.webp +0 -0
  44. package/{assets → frontend/assets}/images/logo_extrasmall.webp +0 -0
  45. package/{assets → frontend/assets}/images/logo_raw.png +0 -0
  46. package/{assets → frontend/assets}/images/logo_small.webp +0 -0
  47. package/{assets → frontend/assets}/mockups/connections.png +0 -0
  48. package/{assets → frontend/assets}/mockups/data.png +0 -0
  49. package/{assets → frontend/assets}/mockups/data_edit.png +0 -0
  50. package/{assets → frontend/assets}/mockups/graph_visualize.png +0 -0
  51. package/{assets → frontend/assets}/mockups/home.png +0 -0
  52. package/{assets → frontend/assets}/mockups/overview.png +0 -0
  53. package/{assets → frontend/assets}/mockups/sql_editor.png +0 -0
  54. package/{assets → frontend/assets}/mockups/structure.png +0 -0
  55. package/{index.html → frontend/index.html} +0 -0
  56. package/{js → frontend/js}/components/actionBar.js +0 -0
  57. package/{js → frontend/js}/components/appShell.js +0 -0
  58. package/{js → frontend/js}/components/badges.js +0 -0
  59. package/{js → frontend/js}/components/bottomTabs.js +1 -1
  60. /package/{js → frontend/js}/components/metricCard.js +0 -0
  61. /package/{js → frontend/js}/components/pageHeader.js +0 -0
  62. /package/{js → frontend/js}/components/statusBar.js +0 -0
  63. /package/{js → frontend/js}/components/structureGraph.js +0 -0
  64. /package/{js → frontend/js}/components/toast.js +0 -0
  65. /package/{js → frontend/js}/components/topNav.js +0 -0
  66. /package/{js → frontend/js}/lib/cytoscapeRuntime.js +0 -0
  67. /package/{js → frontend/js}/router.js +0 -0
  68. /package/{js → frontend/js}/views/connections.js +0 -0
  69. /package/{js → frontend/js}/views/landing.js +0 -0
  70. /package/{js → frontend/js}/views/settings.js +0 -0
  71. /package/{styles → frontend/styles}/base.css +0 -0
  72. /package/{styles → frontend/styles}/layout.css +0 -0
  73. /package/{styles → frontend/styles}/tokens.css +0 -0
  74. /package/{styles → frontend/styles}/views.css +0 -0
  75. /package/{data → server/data}/.gitkeep +0 -0
@@ -0,0 +1,228 @@
1
+ import {
2
+ escapeHtml,
3
+ formatNumber,
4
+ } from "../utils/format.js";
5
+ import { renderStatusBadge } from "./badges.js";
6
+
7
+ function getQueryTypeTone(queryType, isDestructive) {
8
+ if (isDestructive) {
9
+ return "alert";
10
+ }
11
+
12
+ if (queryType === "select") {
13
+ return "success";
14
+ }
15
+
16
+ if (queryType === "pragma") {
17
+ return "primary";
18
+ }
19
+
20
+ return "muted";
21
+ }
22
+
23
+ function renderHistoryItem(item, activeHistoryId, selectedHistoryId) {
24
+ const isActive = Number(activeHistoryId) === Number(item.id);
25
+ const isSelected = Number(selectedHistoryId) === Number(item.id);
26
+ const visibleTables = (item.tablesDetected ?? []).slice(0, 3);
27
+
28
+ return `
29
+ <article class="query-history-item ${isActive ? "is-active" : ""} ${
30
+ item.lastRun?.status === "error" ? "is-error" : ""
31
+ }">
32
+ <button
33
+ class="query-history-item-hit ${isActive ? "is-active" : ""}"
34
+ data-action="select-query-history-item"
35
+ data-history-id="${escapeHtml(item.id)}"
36
+ type="button"
37
+ >
38
+ <div class="flex flex-wrap items-center gap-2">
39
+ <span class="truncate font-headline text-sm font-bold uppercase tracking-tight text-on-surface">
40
+ ${escapeHtml(item.displayTitle)}
41
+ </span>
42
+ ${renderStatusBadge(item.queryType, getQueryTypeTone(item.queryType, item.isDestructive))}
43
+ ${item.isSaved ? renderStatusBadge("saved", "primary") : ""}
44
+ ${item.isDestructive ? renderStatusBadge("destructive", "alert") : ""}
45
+ </div>
46
+ <p class="query-history-sql-preview mt-2 text-left font-mono text-xs leading-5 text-on-surface-variant/75">
47
+ ${escapeHtml(item.previewSql)}
48
+ </p>
49
+ </button>
50
+ <div class="flex items-center justify-between gap-3 border-t border-outline-variant/10 px-3 pb-3 pt-2">
51
+ <div class="min-w-0 flex flex-wrap gap-2 text-[10px] font-mono uppercase tracking-[0.14em] text-on-surface-variant/55">
52
+ ${visibleTables
53
+ .map(
54
+ (tableName) => `
55
+ <span class="border border-outline-variant/20 bg-surface-highest px-2 py-1">
56
+ ${escapeHtml(tableName)}
57
+ </span>
58
+ `
59
+ )
60
+ .join("")}
61
+ </div>
62
+ <div class="flex items-center gap-1">
63
+ <button
64
+ class="query-history-icon-button"
65
+ data-action="open-query-history"
66
+ data-history-id="${escapeHtml(item.id)}"
67
+ title="Open in editor"
68
+ type="button"
69
+ >
70
+ <span class="material-symbols-outlined text-[18px]">edit_note</span>
71
+ </button>
72
+ <button
73
+ class="query-history-icon-button"
74
+ data-action="run-query-history"
75
+ data-history-id="${escapeHtml(item.id)}"
76
+ title="Run query"
77
+ type="button"
78
+ >
79
+ <span class="material-symbols-outlined text-[18px]">play_arrow</span>
80
+ </button>
81
+ <button
82
+ class="query-history-icon-button ${item.isSaved ? "is-active" : ""}"
83
+ data-action="toggle-query-history-saved"
84
+ data-history-id="${escapeHtml(item.id)}"
85
+ data-next-value="${item.isSaved ? "false" : "true"}"
86
+ title="${item.isSaved ? "Remove from saved" : "Save query"}"
87
+ type="button"
88
+ >
89
+ <span class="material-symbols-outlined text-[18px]">
90
+ ${item.isSaved ? "bookmark" : "bookmark_add"}
91
+ </span>
92
+ </button>
93
+ <button
94
+ class="query-history-icon-button ${isSelected ? "is-active" : ""}"
95
+ data-action="select-query-history-item"
96
+ data-history-id="${escapeHtml(item.id)}"
97
+ title="Open query detail"
98
+ type="button"
99
+ >
100
+ <span class="material-symbols-outlined text-[18px]">info</span>
101
+ </button>
102
+ </div>
103
+ </div>
104
+ </article>
105
+ `;
106
+ }
107
+
108
+ function renderQueryHistoryTabs(activeTab, historyTotal) {
109
+ const tabs = [
110
+ { id: "recent", label: "Recent" },
111
+ { id: "saved", label: "Saved" },
112
+ { id: "failed", label: "Failed" },
113
+ ];
114
+
115
+ return `
116
+ <div class="flex items-center gap-2">
117
+ ${tabs
118
+ .map(
119
+ (tab) => `
120
+ <button
121
+ class="query-history-tab ${activeTab === tab.id ? "is-active" : ""}"
122
+ data-action="set-query-history-tab"
123
+ data-tab="${tab.id}"
124
+ type="button"
125
+ >
126
+ ${escapeHtml(tab.label)}
127
+ </button>
128
+ `
129
+ )
130
+ .join("")}
131
+ <span class="ml-auto text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/50">
132
+ ${escapeHtml(formatNumber(historyTotal))}
133
+ </span>
134
+ </div>
135
+ `;
136
+ }
137
+
138
+ export function renderQueryHistoryPanel({
139
+ items = [],
140
+ loading = false,
141
+ loadingMore = false,
142
+ error = null,
143
+ activeTab = "recent",
144
+ search = "",
145
+ total = 0,
146
+ hasMore = false,
147
+ activeHistoryId = null,
148
+ selectedHistoryId = null,
149
+ }) {
150
+ return `
151
+ <aside class="query-history-panel border-l border-outline-variant/10 bg-surface-container-lowest">
152
+ <div class="border-b border-outline-variant/10 px-4 py-4">
153
+ <div class="flex items-center gap-2">
154
+ <span class="material-symbols-outlined text-[18px] text-primary-container">history</span>
155
+ <span class="font-headline text-xs font-black uppercase tracking-[0.18em] text-primary-container">
156
+ Query History
157
+ </span>
158
+ </div>
159
+ <div class="mt-4">${renderQueryHistoryTabs(activeTab, total)}</div>
160
+ <label class="mt-4 block">
161
+ <span class="sr-only">Search query history</span>
162
+ <input
163
+ class="w-full border border-outline-variant/20 bg-surface-container px-3 py-2 text-sm text-on-surface outline-none placeholder:text-on-surface-variant/35 focus:border-primary-container"
164
+ data-bind="query-history-search"
165
+ placeholder="Search SQL, titles, notes..."
166
+ type="search"
167
+ value="${escapeHtml(search)}"
168
+ />
169
+ </label>
170
+ </div>
171
+ <div class="custom-scrollbar min-h-0 flex-1 overflow-auto px-3 py-3">
172
+ ${
173
+ error
174
+ ? `
175
+ <div class="border border-error/30 bg-error-container/20 px-4 py-3 text-sm text-error">
176
+ ${escapeHtml(error.message)}
177
+ </div>
178
+ `
179
+ : ""
180
+ }
181
+ ${
182
+ !loading && !items.length
183
+ ? `
184
+ <div class="flex h-full min-h-[240px] flex-col items-center justify-center px-6 text-center">
185
+ <span class="material-symbols-outlined text-4xl text-on-surface-variant/25">manage_search</span>
186
+ <p class="mt-4 font-headline text-lg font-bold uppercase tracking-tight text-on-surface">
187
+ No Matching Queries
188
+ </p>
189
+ <p class="mt-2 max-w-xs text-sm leading-6 text-on-surface-variant/65">
190
+ Executed statements will appear here once they run against the active database.
191
+ </p>
192
+ </div>
193
+ `
194
+ : ""
195
+ }
196
+ <div class="space-y-3">
197
+ ${items
198
+ .map((item) => renderHistoryItem(item, activeHistoryId, selectedHistoryId))
199
+ .join("")}
200
+ </div>
201
+ ${
202
+ loading
203
+ ? `
204
+ <div class="mt-4 text-center text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/50">
205
+ Loading query history...
206
+ </div>
207
+ `
208
+ : ""
209
+ }
210
+ ${
211
+ hasMore
212
+ ? `
213
+ <div class="mt-4 flex justify-center">
214
+ <button
215
+ class="border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant transition-colors hover:border-primary-container hover:text-primary-container"
216
+ data-action="load-more-query-history"
217
+ type="button"
218
+ >
219
+ ${loadingMore ? "Loading..." : "Load More"}
220
+ </button>
221
+ </div>
222
+ `
223
+ : ""
224
+ }
225
+ </div>
226
+ </aside>
227
+ `;
228
+ }
@@ -1,9 +1,39 @@
1
1
  import { renderDataGrid } from "./dataGrid.js";
2
2
  import { escapeHtml, formatCellValue, formatNumber } from "../utils/format.js";
3
3
 
4
+ function getSortIcon(columnName, sortColumn, sortDirection) {
5
+ if (columnName !== sortColumn) {
6
+ return "unfold_more";
7
+ }
8
+
9
+ return sortDirection === "desc" ? "south" : "north";
10
+ }
11
+
12
+ function renderSortableHeader(columnName, sortColumn, sortDirection) {
13
+ const isActive = columnName === sortColumn;
14
+
15
+ return `
16
+ <button
17
+ class="flex w-full items-center justify-between gap-2 text-left transition-colors ${
18
+ isActive ? "text-primary-container" : "text-on-surface-variant hover:text-primary-container"
19
+ }"
20
+ data-action="sort-editor-results-column"
21
+ data-column-name="${escapeHtml(columnName)}"
22
+ type="button"
23
+ >
24
+ <span class="truncate">${escapeHtml(columnName)}</span>
25
+ <span class="material-symbols-outlined text-sm leading-none">${getSortIcon(
26
+ columnName,
27
+ sortColumn,
28
+ sortDirection
29
+ )}</span>
30
+ </button>
31
+ `;
32
+ }
33
+
4
34
  export function renderQueryResultsPane(
5
35
  result,
6
- { exporting = false, selectedRowIndex = null, editable = false, editStatusMessage = "" } = {}
36
+ { selectedRowIndex = null, editable = false, sortColumn = null, sortDirection = null } = {}
7
37
  ) {
8
38
  if (!result) {
9
39
  return `
@@ -17,9 +47,9 @@ export function renderQueryResultsPane(
17
47
  }
18
48
 
19
49
  const columns = (result.columns ?? []).map((columnName) => ({
20
- label: escapeHtml(columnName),
21
50
  headerClassName:
22
51
  "border-b-2 border-primary-container px-4 py-3 text-[10px] font-bold uppercase tracking-widest",
52
+ renderHeader: () => renderSortableHeader(columnName, sortColumn, sortDirection),
23
53
  cellClassName: "px-4 py-3 align-top text-on-surface",
24
54
  render: (row) => {
25
55
  const value = formatCellValue(row[columnName]);
@@ -32,50 +62,6 @@ export function renderQueryResultsPane(
32
62
 
33
63
  return `
34
64
  <div class="relative flex h-full min-h-0 flex-col overflow-hidden bg-surface-container">
35
- <div class="flex items-center justify-between gap-4 bg-surface-container-high px-4 py-2">
36
- <div class="flex items-center gap-4">
37
- <div class="flex items-center gap-2 text-primary-container">
38
- <span class="material-symbols-outlined text-sm">database</span>
39
- <span class="text-[10px] font-bold uppercase tracking-widest">Query Results</span>
40
- </div>
41
- <div class="h-3 w-px bg-outline-variant/30"></div>
42
- <div class="text-[10px] font-mono text-on-surface-variant/60">
43
- ROWS_RETURNED: ${escapeHtml(formatNumber(result.rows?.length ?? 0))}
44
- </div>
45
- <div class="text-[10px] font-mono text-on-surface-variant/60">
46
- AFFECTED: ${escapeHtml(formatNumber(result.affectedRowCount ?? 0))}
47
- </div>
48
- <div class="text-[10px] font-mono text-on-surface-variant/60">
49
- EXEC: ${escapeHtml(String(result.timingMs ?? 0))}ms
50
- </div>
51
- ${
52
- editStatusMessage
53
- ? `
54
- <div class="h-3 w-px bg-outline-variant/30"></div>
55
- <div class="text-[10px] font-mono text-on-surface-variant/60">
56
- ${escapeHtml(editStatusMessage)}
57
- </div>
58
- `
59
- : ""
60
- }
61
- </div>
62
- <div class="flex gap-4 text-[10px] font-mono uppercase">
63
- <button
64
- class="text-on-surface-variant transition-colors hover:text-primary-container"
65
- data-action="export-query-csv"
66
- type="button"
67
- >
68
- ${exporting ? "Exporting..." : "Export CSV"}
69
- </button>
70
- <button
71
- class="text-on-surface-variant transition-colors hover:text-primary-container"
72
- data-action="clear-results"
73
- type="button"
74
- >
75
- Clear Results
76
- </button>
77
- </div>
78
- </div>
79
65
  <div class="custom-scrollbar min-h-0 flex-1 overflow-auto bg-surface-container-lowest">
80
66
  ${
81
67
  result.columns?.length
@@ -1,16 +1,87 @@
1
1
  import { escapeHtml } from "../utils/format.js";
2
2
 
3
+ function getJsonPreview(value) {
4
+ if (value === null || value === undefined) {
5
+ return null;
6
+ }
7
+
8
+ if (typeof value === "object") {
9
+ return JSON.stringify(value, null, 2);
10
+ }
11
+
12
+ const text = String(value).trim();
13
+
14
+ if (!text || !["{", "["].includes(text[0])) {
15
+ return null;
16
+ }
17
+
18
+ try {
19
+ const parsed = JSON.parse(text);
20
+
21
+ if (!parsed || typeof parsed !== "object") {
22
+ return null;
23
+ }
24
+
25
+ return JSON.stringify(parsed, null, 2);
26
+ } catch (error) {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function renderJsonViewer(prettyJson, title = "JSON Viewer") {
32
+ return `
33
+ <div class="border border-outline-variant/10 bg-surface-container px-4 py-4">
34
+ <div class="text-[10px] font-mono uppercase tracking-[0.18em] text-primary-container/75">
35
+ ${escapeHtml(title)}
36
+ </div>
37
+ <pre class="custom-scrollbar mt-3 max-h-[18rem] overflow-auto whitespace-pre-wrap break-words border border-outline-variant/10 bg-surface-container-lowest px-4 py-4 font-mono text-xs leading-6 text-on-surface">${escapeHtml(
38
+ prettyJson
39
+ )}</pre>
40
+ </div>
41
+ `;
42
+ }
43
+
3
44
  function renderReadonlyField(label, value) {
45
+ const jsonPreview = getJsonPreview(value);
46
+
4
47
  return `
5
48
  <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
6
49
  <div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
7
50
  ${escapeHtml(label)}
8
51
  </div>
9
- <div class="mt-2 text-sm text-on-surface">${escapeHtml(value)}</div>
52
+ ${
53
+ jsonPreview
54
+ ? `<div class="mt-2">${renderJsonViewer(jsonPreview)}</div>`
55
+ : `<div class="mt-2 text-sm text-on-surface">${escapeHtml(value)}</div>`
56
+ }
10
57
  </div>
11
58
  `;
12
59
  }
13
60
 
61
+ function renderEditableField(field) {
62
+ const jsonPreview = getJsonPreview(field.value);
63
+
64
+ return `
65
+ <label class="block space-y-2">
66
+ <span class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
67
+ ${escapeHtml(field.label ?? field.name)}
68
+ </span>
69
+ ${
70
+ jsonPreview
71
+ ? renderJsonViewer(jsonPreview, "JSON Preview")
72
+ : ""
73
+ }
74
+ <textarea
75
+ class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container ${
76
+ jsonPreview ? "min-h-[14rem] font-mono leading-6" : "min-h-[56px]"
77
+ }"
78
+ name="field:${escapeHtml(field.name)}"
79
+ spellcheck="false"
80
+ >${escapeHtml(field.value ?? "")}</textarea>
81
+ </label>
82
+ `;
83
+ }
84
+
14
85
  export function renderRowEditorPanel({
15
86
  title,
16
87
  sectionLabel = "Row Editor",
@@ -137,19 +208,7 @@ export function renderRowEditorPanel({
137
208
  ? `
138
209
  <div class="space-y-4">
139
210
  ${editableFields
140
- .map(
141
- (field) => `
142
- <label class="block space-y-2">
143
- <span class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
144
- ${escapeHtml(field.label ?? field.name)}
145
- </span>
146
- <textarea
147
- class="min-h-[56px] w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
148
- name="field:${escapeHtml(field.name)}"
149
- >${escapeHtml(field.value ?? "")}</textarea>
150
- </label>
151
- `
152
- )
211
+ .map((field) => renderEditableField(field))
153
212
  .join("")}
154
213
  </div>
155
214
  `
@@ -1,4 +1,5 @@
1
1
  import { escapeHtml } from "../utils/format.js";
2
+ import { renderConnectionLogo } from "./connectionLogo.js";
2
3
 
3
4
  const sidebarItems = [
4
5
  { label: "Connections", href: "#/connections", key: "connections", icon: "database" },
@@ -40,9 +41,13 @@ export function renderSidebar(state) {
40
41
  </nav>
41
42
  <div class="sidebar-footer">
42
43
  <div class="sidebar-footer-card">
43
- <div class="sidebar-footer-mark">
44
- <span class="material-symbols-outlined text-[15px]">memory</span>
45
- </div>
44
+ ${renderConnectionLogo(activeConnection, {
45
+ containerClass:
46
+ "sidebar-footer-mark overflow-hidden bg-primary-container text-on-primary",
47
+ imageClassName: "h-full w-full object-cover",
48
+ iconClassName: "text-[15px]",
49
+ icon: "memory",
50
+ })}
46
51
  <div class="min-w-0">
47
52
  <p class="truncate text-[10px] font-bold text-on-surface">
48
53
  ${escapeHtml(activeConnection?.label ?? "NO_ACTIVE_DATABASE")}