sqlite-hub 0.4.0 → 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.
package/changelog.md CHANGED
@@ -1,3 +1,10 @@
1
+ # v0.5.0
2
+
3
+ - sql query history
4
+ - open in finder in overview
5
+ - sort columns in data & sql_editor
6
+ - json view in row editor
7
+
1
8
  # v0.4.0
2
9
 
3
10
  - search in data
@@ -123,6 +123,12 @@ export function getOverview() {
123
123
  return request("/api/db/overview");
124
124
  }
125
125
 
126
+ export function openOverviewInFinder() {
127
+ return request("/api/db/overview/open-in-finder", {
128
+ method: "POST",
129
+ });
130
+ }
131
+
126
132
  export function getDbStatus() {
127
133
  return request("/api/db/status");
128
134
  }
@@ -134,16 +140,92 @@ export function executeSql(sql) {
134
140
  });
135
141
  }
136
142
 
137
- export function getSqlHistory() {
138
- return request("/api/sql/history");
143
+ export function getQueryHistory(options = {}) {
144
+ const params = new URLSearchParams();
145
+
146
+ if (options.tab) {
147
+ params.set("tab", String(options.tab));
148
+ }
149
+
150
+ if (options.limit !== undefined) {
151
+ params.set("limit", String(options.limit));
152
+ }
153
+
154
+ if (options.offset !== undefined) {
155
+ params.set("offset", String(options.offset));
156
+ }
157
+
158
+ if (options.search) {
159
+ params.set("search", String(options.search));
160
+ }
161
+
162
+ if (options.queryType) {
163
+ params.set("queryType", String(options.queryType));
164
+ }
165
+
166
+ if (options.onlySaved) {
167
+ params.set("onlySaved", "true");
168
+ }
169
+
170
+ if (options.onlyFavorites) {
171
+ params.set("onlyFavorites", "true");
172
+ }
173
+
174
+ const query = params.toString();
175
+ return request(`/api/sql/history${query ? `?${query}` : ""}`);
139
176
  }
140
177
 
141
- export function clearSqlHistory() {
178
+ export function clearQueryHistory() {
142
179
  return request("/api/sql/history", {
143
180
  method: "DELETE",
144
181
  });
145
182
  }
146
183
 
184
+ export function getQueryHistoryItem(historyId) {
185
+ return request(`/api/sql/history/${encodeURIComponent(historyId)}`);
186
+ }
187
+
188
+ export function getQueryHistoryRuns(historyId, options = {}) {
189
+ const params = new URLSearchParams();
190
+
191
+ if (options.limit !== undefined) {
192
+ params.set("limit", String(options.limit));
193
+ }
194
+
195
+ const query = params.toString();
196
+
197
+ return request(
198
+ `/api/sql/history/${encodeURIComponent(historyId)}/runs${query ? `?${query}` : ""}`
199
+ );
200
+ }
201
+
202
+ export function toggleQueryHistorySaved(historyId, value) {
203
+ return request(`/api/sql/history/${encodeURIComponent(historyId)}/saved`, {
204
+ method: "PATCH",
205
+ body: { value },
206
+ });
207
+ }
208
+
209
+ export function renameQueryHistoryItem(historyId, title) {
210
+ return request(`/api/sql/history/${encodeURIComponent(historyId)}/title`, {
211
+ method: "PATCH",
212
+ body: { title },
213
+ });
214
+ }
215
+
216
+ export function updateQueryHistoryNotes(historyId, notes) {
217
+ return request(`/api/sql/history/${encodeURIComponent(historyId)}/notes`, {
218
+ method: "PATCH",
219
+ body: { notes },
220
+ });
221
+ }
222
+
223
+ export function deleteQueryHistoryItem(historyId) {
224
+ return request(`/api/sql/history/${encodeURIComponent(historyId)}`, {
225
+ method: "DELETE",
226
+ });
227
+ }
228
+
147
229
  export function getStructureOverview() {
148
230
  return request("/api/structure");
149
231
  }
@@ -167,6 +249,14 @@ export function getDataTable(tableName, options = {}) {
167
249
  params.set("offset", String(options.offset));
168
250
  }
169
251
 
252
+ if (options.sortColumn) {
253
+ params.set("sortColumn", String(options.sortColumn));
254
+ }
255
+
256
+ if (options.sortDirection) {
257
+ params.set("sortDirection", String(options.sortDirection));
258
+ }
259
+
170
260
  const query = params.toString();
171
261
 
172
262
  return request(`/api/data/${encodeURIComponent(tableName)}${query ? `?${query}` : ""}`);
@@ -205,10 +295,14 @@ export function downloadQueryCsv(sql) {
205
295
  });
206
296
  }
207
297
 
208
- export function downloadTableCsv(tableName) {
298
+ export function downloadTableCsv(tableName, options = {}) {
209
299
  return download("/api/export/table.csv", {
210
300
  method: "POST",
211
- body: { tableName },
301
+ body: {
302
+ tableName,
303
+ sortColumn: options.sortColumn,
304
+ sortDirection: options.sortDirection,
305
+ },
212
306
  fallbackFilename: `${tableName || "table"}.csv`,
213
307
  });
214
308
  }
@@ -16,24 +16,29 @@ import {
16
16
  clearDataRowSelection,
17
17
  clearEditorRowSelection,
18
18
  clearEditorResults,
19
- clearSqlHistoryStateAndData,
19
+ clearQueryHistorySelection,
20
20
  closeModal,
21
+ deleteQueryHistoryStateItem,
21
22
  dismissToast,
22
23
  executeCurrentQuery,
23
24
  exportCurrentDataTableCsv,
24
25
  exportCurrentQueryCsv,
25
26
  getState,
26
27
  initializeApp,
27
- loadQueryFromHistory,
28
+ loadMoreQueryHistory,
28
29
  openModal,
30
+ openOverviewInFinder,
31
+ openQueryHistoryInEditor,
29
32
  openDeleteDataRowModal,
30
33
  openDeleteEditorRowModal,
31
34
  openEditConnectionModal,
32
35
  refreshCurrentRoute,
33
36
  removeConnection,
37
+ runQueryHistoryItem,
34
38
  selectDataRow,
35
39
  selectEditorRow,
36
40
  selectConnection,
41
+ selectQueryHistoryItem,
37
42
  selectStructureEntry,
38
43
  setDataPage,
39
44
  setDataPageSize,
@@ -41,7 +46,13 @@ import {
41
46
  setDataSearchQuery,
42
47
  setCurrentQuery,
43
48
  setEditorTab,
49
+ sortDataTableByColumn,
50
+ sortEditorResultsByColumn,
51
+ setQueryHistorySearchInput,
52
+ setQueryHistoryTab,
44
53
  setRoute,
54
+ saveQueryHistoryNotes,
55
+ saveQueryHistoryTitle,
45
56
  submitCreateConnection,
46
57
  submitDeleteRowConfirmation,
47
58
  submitDataRowUpdate,
@@ -50,6 +61,7 @@ import {
50
61
  submitImportSql,
51
62
  submitOpenConnection,
52
63
  subscribe,
64
+ toggleQueryHistorySavedState,
53
65
  } from "./store.js";
54
66
  import { renderConnectionsView } from "./views/connections.js";
55
67
  import { renderDataView } from "./views/data.js";
@@ -338,6 +350,9 @@ async function handleAction(actionNode) {
338
350
  case "create-backup":
339
351
  await createActiveConnectionBackup();
340
352
  return;
353
+ case "open-overview-in-finder":
354
+ await openOverviewInFinder();
355
+ return;
341
356
  case "execute-query": {
342
357
  await executeEditorQueryAndNavigate();
343
358
  return;
@@ -350,11 +365,57 @@ async function handleAction(actionNode) {
350
365
  return;
351
366
  case "clear-query":
352
367
  clearCurrentQuery();
368
+ if (getState().route.name === "editorResults") {
369
+ router.navigate("/editor");
370
+ }
353
371
  return;
354
372
  case "clear-results":
355
373
  clearEditorResults();
356
374
  router.navigate("/editor");
357
375
  return;
376
+ case "select-query-history-item":
377
+ if (actionNode.dataset.historyId) {
378
+ await selectQueryHistoryItem(actionNode.dataset.historyId);
379
+ }
380
+ return;
381
+ case "clear-query-history-selection":
382
+ clearQueryHistorySelection();
383
+ return;
384
+ case "set-query-history-tab":
385
+ if (actionNode.dataset.tab) {
386
+ await setQueryHistoryTab(actionNode.dataset.tab);
387
+ }
388
+ return;
389
+ case "load-more-query-history":
390
+ await loadMoreQueryHistory();
391
+ return;
392
+ case "open-query-history":
393
+ if (actionNode.dataset.historyId && openQueryHistoryInEditor(actionNode.dataset.historyId)) {
394
+ router.navigate("/editor");
395
+ }
396
+ return;
397
+ case "run-query-history":
398
+ if (actionNode.dataset.historyId) {
399
+ const success = await runQueryHistoryItem(actionNode.dataset.historyId);
400
+ router.navigate(success ? "/editor/results" : "/editor");
401
+ }
402
+ return;
403
+ case "toggle-query-history-saved":
404
+ if (actionNode.dataset.historyId) {
405
+ await toggleQueryHistorySavedState(
406
+ actionNode.dataset.historyId,
407
+ actionNode.dataset.nextValue === "true"
408
+ );
409
+ }
410
+ return;
411
+ case "delete-query-history":
412
+ if (
413
+ actionNode.dataset.historyId &&
414
+ window.confirm("Delete this query history item and all recorded runs?")
415
+ ) {
416
+ await deleteQueryHistoryStateItem(actionNode.dataset.historyId);
417
+ }
418
+ return;
358
419
  case "set-editor-tab": {
359
420
  const tab = actionNode.dataset.tab;
360
421
  if (!tab) {
@@ -364,9 +425,6 @@ async function handleAction(actionNode) {
364
425
  router.navigate(tab === "results" ? "/editor/results" : "/editor");
365
426
  return;
366
427
  }
367
- case "clear-sql-history":
368
- await clearSqlHistoryStateAndData();
369
- return;
370
428
  case "export-query-csv":
371
429
  await exportCurrentQueryCsv();
372
430
  return;
@@ -399,6 +457,16 @@ async function handleAction(actionNode) {
399
457
  await setDataPage(actionNode.dataset.page);
400
458
  }
401
459
  return;
460
+ case "sort-data-column":
461
+ if (actionNode.dataset.columnName) {
462
+ await sortDataTableByColumn(actionNode.dataset.columnName);
463
+ }
464
+ return;
465
+ case "sort-editor-results-column":
466
+ if (actionNode.dataset.columnName) {
467
+ sortEditorResultsByColumn(actionNode.dataset.columnName);
468
+ }
469
+ return;
402
470
  case "set-data-page-size":
403
471
  if (actionNode.dataset.pageSize) {
404
472
  await setDataPageSize(actionNode.dataset.pageSize);
@@ -483,6 +551,11 @@ document.addEventListener("input", (event) => {
483
551
 
484
552
  if (bindNode.dataset.bind === "data-search-query") {
485
553
  setDataSearchQuery(bindNode.value);
554
+ return;
555
+ }
556
+
557
+ if (bindNode.dataset.bind === "query-history-search") {
558
+ setQueryHistorySearchInput(bindNode.value);
486
559
  }
487
560
  });
488
561
 
@@ -507,12 +580,6 @@ document.addEventListener("change", (event) => {
507
580
  return;
508
581
  }
509
582
 
510
- if (bindNode.dataset.bind === "history-entry" && bindNode.value) {
511
- loadQueryFromHistory(bindNode.value);
512
- bindNode.value = "";
513
- return;
514
- }
515
-
516
583
  if (bindNode.dataset.bind === "data-search-column") {
517
584
  setDataSearchColumn(bindNode.value);
518
585
  }
@@ -634,6 +701,18 @@ document.addEventListener("submit", async (event) => {
634
701
  );
635
702
  return;
636
703
  }
704
+ case "save-query-history-title":
705
+ await saveQueryHistoryTitle(
706
+ String(formData.get("historyId") ?? ""),
707
+ String(formData.get("title") ?? "")
708
+ );
709
+ return;
710
+ case "save-query-history-notes":
711
+ await saveQueryHistoryNotes(
712
+ String(formData.get("historyId") ?? ""),
713
+ String(formData.get("notes") ?? "")
714
+ );
715
+ return;
637
716
  default:
638
717
  }
639
718
  });
@@ -3,8 +3,8 @@ import { escapeHtml } from "../utils/format.js";
3
3
  export function renderBottomTabs(activeTab, counts = {}) {
4
4
  const tabs = [
5
5
  { key: "results", label: "results", meta: counts.resultRows ?? 0 },
6
- { key: "messages", label: "messages", meta: counts.messages ?? 0 },
7
6
  { key: "performance", label: "performance", meta: counts.statementCount ?? 0 },
7
+ { key: "messages", label: "messages", meta: counts.messages ?? 0 },
8
8
  ];
9
9
 
10
10
  return `
@@ -14,9 +14,9 @@ export function renderDataGrid({
14
14
  <tr class="${headerRowClass}">
15
15
  ${columns
16
16
  .map(
17
- (column) => `
18
- <th class="${column.headerClassName ?? ""}">
19
- ${column.label}
17
+ (column) => `
18
+ <th class="${column.headerClassName ?? ""}" ${column.headerAttrs ?? ""}>
19
+ ${column.renderHeader ? column.renderHeader() : column.label}
20
20
  </th>
21
21
  `
22
22
  )
@@ -9,23 +9,6 @@ function renderLineNumbers(query) {
9
9
  .join("");
10
10
  }
11
11
 
12
- function renderHistoryOptions(history) {
13
- if (!history.length) {
14
- return '<option value="">No recent statements</option>';
15
- }
16
-
17
- return [
18
- '<option value="">Load recent statement...</option>',
19
- ...history.map(
20
- (entry) => `
21
- <option value="${escapeHtml(entry.id)}">
22
- ${escapeHtml(entry.sql.replace(/\s+/g, " ").slice(0, 96))}
23
- </option>
24
- `
25
- ),
26
- ].join("");
27
- }
28
-
29
12
  function renderHighlightedQuery(query) {
30
13
  if (query) {
31
14
  return highlightSql(query);
@@ -34,13 +17,41 @@ function renderHighlightedQuery(query) {
34
17
  return '<span class="text-on-surface-variant/35">SELECT name FROM sqlite_master WHERE type = \'table\';</span>';
35
18
  }
36
19
 
20
+ function renderEditorSurface({ query }) {
21
+ return `
22
+ <div class="flex min-h-0 flex-1 overflow-hidden">
23
+ <div class="flex w-12 flex-col items-center bg-surface-container-lowest py-4 font-mono text-xs select-none text-outline-variant/30">
24
+ ${renderLineNumbers(query)}
25
+ </div>
26
+ <div class="relative flex-1 overflow-hidden bg-surface-container p-6 font-mono text-sm leading-relaxed">
27
+ <div class="pointer-events-none absolute right-0 top-0 p-4 opacity-5">
28
+ <span class="material-symbols-outlined text-[120px] font-thin">terminal</span>
29
+ </div>
30
+ <div class="query-editor-layer relative z-10 h-full min-h-[140px]">
31
+ <pre
32
+ aria-hidden="true"
33
+ class="query-editor-highlight"
34
+ data-query-editor-highlight
35
+ >${renderHighlightedQuery(query)}</pre>
36
+ <textarea
37
+ class="query-editor-input custom-scrollbar relative z-10 h-full min-h-[140px] w-full resize-none border-none focus:ring-0"
38
+ data-bind="current-query"
39
+ placeholder="SELECT name FROM sqlite_master WHERE type = 'table';"
40
+ spellcheck="false"
41
+ >${escapeHtml(query)}</textarea>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ `;
46
+ }
47
+
37
48
  export function renderQueryEditor({
38
49
  query,
39
50
  title,
40
51
  executing = false,
41
52
  exporting = false,
42
- history = [],
43
53
  historyLoading = false,
54
+ historyTotal = 0,
44
55
  }) {
45
56
  const left = `
46
57
  <div class="flex items-center gap-2 bg-surface-container-lowest px-3 py-1">
@@ -51,24 +62,11 @@ export function renderQueryEditor({
51
62
  </div>
52
63
  <div class="hidden items-center gap-2 text-[10px] font-mono uppercase tracking-widest text-on-surface-variant/40 md:flex">
53
64
  <span class="material-symbols-outlined text-xs">history</span>
54
- ${historyLoading ? "Loading history..." : `${history.length} statements in history`}
65
+ ${historyLoading ? "Loading history..." : `${historyTotal} queries tracked`}
55
66
  </div>
56
67
  `;
57
68
 
58
69
  const right = `
59
- <select
60
- class="min-w-[220px] border border-outline-variant/20 bg-surface-container-lowest px-3 py-2 text-[10px] font-mono uppercase tracking-[0.14em] text-on-surface-variant outline-none"
61
- data-bind="history-entry"
62
- >
63
- ${renderHistoryOptions(history)}
64
- </select>
65
- <button
66
- class="px-4 py-1.5 text-[10px] font-bold uppercase tracking-widest text-on-surface hover:bg-surface-container-highest transition-colors"
67
- data-action="clear-sql-history"
68
- type="button"
69
- >
70
- Clear History
71
- </button>
72
70
  <button
73
71
  class="px-4 py-1.5 text-[10px] font-bold uppercase tracking-widest text-on-surface hover:bg-surface-container-highest transition-colors"
74
72
  data-action="clear-query"
@@ -93,7 +91,7 @@ export function renderQueryEditor({
93
91
  `;
94
92
 
95
93
  return `
96
- <div class="flex h-full flex-col">
94
+ <div class="flex h-full min-h-0 flex-col">
97
95
  <div class="bg-surface-container-low px-6 py-3">
98
96
  ${renderActionBar({
99
97
  left,
@@ -101,28 +99,8 @@ export function renderQueryEditor({
101
99
  className: "flex-wrap",
102
100
  })}
103
101
  </div>
104
- <div class="flex flex-1 overflow-hidden">
105
- <div class="flex w-12 flex-col items-center bg-surface-container-lowest py-4 font-mono text-xs select-none text-outline-variant/30">
106
- ${renderLineNumbers(query)}
107
- </div>
108
- <div class="relative flex-1 overflow-hidden bg-surface-container p-6 font-mono text-sm leading-relaxed">
109
- <div class="pointer-events-none absolute right-0 top-0 p-4 opacity-5">
110
- <span class="material-symbols-outlined text-[120px] font-thin">terminal</span>
111
- </div>
112
- <div class="query-editor-layer relative z-10 h-full min-h-[140px]">
113
- <pre
114
- aria-hidden="true"
115
- class="query-editor-highlight"
116
- data-query-editor-highlight
117
- >${renderHighlightedQuery(query)}</pre>
118
- <textarea
119
- class="query-editor-input custom-scrollbar relative z-10 h-full min-h-[140px] w-full resize-none border-none focus:ring-0"
120
- data-bind="current-query"
121
- placeholder="SELECT name FROM sqlite_master WHERE type = 'table';"
122
- spellcheck="false"
123
- >${escapeHtml(query)}</textarea>
124
- </div>
125
- </div>
102
+ <div class="flex min-h-0 flex-1 flex-col">
103
+ ${renderEditorSurface({ query })}
126
104
  </div>
127
105
  </div>
128
106
  `;