sqlite-hub 1.0.0 → 1.1.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.
Files changed (47) hide show
  1. package/README.md +47 -33
  2. package/bin/sqlite-hub.js +127 -47
  3. package/frontend/js/api.js +6 -0
  4. package/frontend/js/app.js +229 -44
  5. package/frontend/js/components/modal.js +17 -1
  6. package/frontend/js/components/queryChartRenderer.js +28 -3
  7. package/frontend/js/components/queryEditor.js +24 -26
  8. package/frontend/js/components/queryHistoryDetail.js +1 -1
  9. package/frontend/js/components/queryHistoryHeader.js +48 -0
  10. package/frontend/js/components/queryHistoryList.js +132 -0
  11. package/frontend/js/components/queryHistoryPanel.js +72 -136
  12. package/frontend/js/components/structureGraph.js +700 -89
  13. package/frontend/js/components/tableDesignerEditor.js +26 -47
  14. package/frontend/js/components/tableDesignerSidebar.js +7 -31
  15. package/frontend/js/components/tableDesignerSqlPreview.js +2 -1
  16. package/frontend/js/store.js +97 -8
  17. package/frontend/js/utils/exportFilenames.js +3 -1
  18. package/frontend/js/views/charts.js +326 -174
  19. package/frontend/js/views/connections.js +59 -64
  20. package/frontend/js/views/data.js +48 -54
  21. package/frontend/js/views/documents.js +62 -23
  22. package/frontend/js/views/editor.js +0 -2
  23. package/frontend/js/views/mediaTagging.js +6 -5
  24. package/frontend/js/views/settings.js +78 -0
  25. package/frontend/js/views/structure.js +48 -32
  26. package/frontend/js/views/tableDesigner.js +45 -1
  27. package/frontend/styles/components.css +259 -54
  28. package/frontend/styles/structure-graph.css +150 -37
  29. package/frontend/styles/tailwind.generated.css +1 -1
  30. package/frontend/styles/tokens.css +2 -0
  31. package/frontend/styles/views.css +75 -38
  32. package/package.json +3 -2
  33. package/server/routes/export.js +89 -22
  34. package/server/routes/externalApi.js +84 -2
  35. package/server/routes/settings.js +37 -28
  36. package/server/services/appInfoService.js +215 -0
  37. package/server/services/databaseCommandService.js +50 -6
  38. package/server/services/sqlite/dataBrowserService.js +11 -3
  39. package/server/services/sqlite/exportService.js +307 -22
  40. package/tests/api-token-auth.test.js +110 -1
  41. package/tests/cli-args.test.js +16 -3
  42. package/tests/database-command-service.test.js +40 -3
  43. package/tests/export-blob.test.js +54 -1
  44. package/tests/export-filenames.test.js +4 -0
  45. package/tests/settings-api-tokens-route.test.js +22 -1
  46. package/tests/settings-metadata.test.js +99 -1
  47. package/tests/settings-view.test.js +28 -0
@@ -0,0 +1,48 @@
1
+ import { escapeHtml } from '../utils/format.js';
2
+
3
+ export function renderQueryHistoryHeader({
4
+ title = 'Query History',
5
+ icon = 'history',
6
+ closeAction = 'toggle-query-history-panel',
7
+ closeTitle = 'Hide query history',
8
+ nextValue = 'false',
9
+ } = {}) {
10
+ return `
11
+ <div class="flex items-center justify-between gap-3">
12
+ <div class="flex items-center gap-2">
13
+ <span class="material-symbols-outlined text-[18px] text-primary-container">${escapeHtml(icon)}</span>
14
+ <span class="font-headline text-xs font-black uppercase tracking-[0.18em] text-primary-container">
15
+ ${escapeHtml(title)}
16
+ </span>
17
+ </div>
18
+ <button
19
+ class="query-history-icon-button"
20
+ data-action="${escapeHtml(closeAction)}"
21
+ data-next-value="${escapeHtml(nextValue)}"
22
+ title="${escapeHtml(closeTitle)}"
23
+ type="button"
24
+ >
25
+ <span class="material-symbols-outlined text-[18px]">close</span>
26
+ </button>
27
+ </div>
28
+ `;
29
+ }
30
+
31
+ export function renderQueryHistorySearch({
32
+ bind = 'query-history-search',
33
+ value = '',
34
+ placeholder = 'Search SQL, titles, notes...',
35
+ } = {}) {
36
+ return `
37
+ <label class="mt-4 block">
38
+ <span class="sr-only">Search query history</span>
39
+ <input
40
+ class="control-input w-full border border-outline-variant/20 bg-surface-container text-sm text-on-surface outline-none placeholder:text-on-surface-variant/35 focus:border-primary-container"
41
+ data-bind="${escapeHtml(bind)}"
42
+ placeholder="${escapeHtml(placeholder)}"
43
+ type="search"
44
+ value="${escapeHtml(value)}"
45
+ />
46
+ </label>
47
+ `;
48
+ }
@@ -0,0 +1,132 @@
1
+ import { escapeHtml, formatNumber } from '../utils/format.js';
2
+
3
+ export function getQueryTypeTone(queryType) {
4
+ if (queryType === 'select' || queryType === 'update') {
5
+ return 'success';
6
+ }
7
+
8
+ if (queryType === 'pragma') {
9
+ return 'primary';
10
+ }
11
+
12
+ return 'muted';
13
+ }
14
+
15
+ export function renderQueryHistoryBadgeRow(markup, { compact = true, className = '' } = {}) {
16
+ const rowClasses = ['query-history-badge-row', compact ? 'query-history-badge-row--compact' : '', className]
17
+ .filter(Boolean)
18
+ .join(' ');
19
+
20
+ return `<div class="${rowClasses}">${markup}</div>`;
21
+ }
22
+
23
+ export function renderQueryHistoryTabs({ tabs = [], activeTab = 'recent', action, count = null, countAttr = '' }) {
24
+ const activeCount = count ?? tabs.find(tab => tab.id === activeTab)?.count ?? 0;
25
+
26
+ return [
27
+ '<div class="query-history-tabs">',
28
+ tabs
29
+ .map(tab =>
30
+ [
31
+ '<button class="query-history-tab ',
32
+ activeTab === tab.id ? 'is-active' : '',
33
+ '" data-action="',
34
+ escapeHtml(action),
35
+ '" data-tab="',
36
+ escapeHtml(tab.id),
37
+ '" type="button">',
38
+ escapeHtml(tab.label),
39
+ '</button>',
40
+ ].join(''),
41
+ )
42
+ .join(''),
43
+ '<span class="query-history-tabs__count" ',
44
+ countAttr,
45
+ '>',
46
+ escapeHtml(formatNumber(activeCount)),
47
+ '</span></div>',
48
+ ].join('');
49
+ }
50
+
51
+ export function renderQueryHistoryIconButton({
52
+ action,
53
+ historyId = null,
54
+ icon,
55
+ title,
56
+ active = false,
57
+ nextValue = null,
58
+ attrs = '',
59
+ }) {
60
+ return [
61
+ '<button class="query-history-icon-button ',
62
+ active ? 'is-active' : '',
63
+ '" data-action="',
64
+ escapeHtml(action),
65
+ '"',
66
+ historyId === null || historyId === undefined ? '' : ` data-history-id="${escapeHtml(historyId)}"`,
67
+ nextValue === null || nextValue === undefined ? '' : ` data-next-value="${escapeHtml(nextValue)}"`,
68
+ attrs ? ` ${attrs}` : '',
69
+ ' title="',
70
+ escapeHtml(title),
71
+ '" type="button"><span class="material-symbols-outlined text-[18px]">',
72
+ escapeHtml(icon),
73
+ '</span></button>',
74
+ ].join('');
75
+ }
76
+
77
+ export function renderQueryHistoryActionGroup(actions = []) {
78
+ return `<div class="query-history-item-actions">${actions.filter(Boolean).join('')}</div>`;
79
+ }
80
+
81
+ export function renderQueryHistoryListItem({
82
+ title,
83
+ preview,
84
+ historyId,
85
+ active = false,
86
+ error = false,
87
+ hitAction,
88
+ hitAttrs = '',
89
+ itemAttrs = '',
90
+ titleAttrs = '',
91
+ badgesMarkup = '',
92
+ footerMetaMarkup = '',
93
+ actionsMarkup = '',
94
+ }) {
95
+ const itemClasses = ['query-history-item', active ? 'is-active' : '', error ? 'is-error' : '']
96
+ .filter(Boolean)
97
+ .join(' ');
98
+ const footerMeta = String(footerMetaMarkup ?? '').trim()
99
+ ? ['<div class="query-history-item-meta">', footerMetaMarkup, '</div>'].join('')
100
+ : '';
101
+
102
+ return [
103
+ '<article class="',
104
+ itemClasses,
105
+ '"',
106
+ itemAttrs ? ` ${itemAttrs}` : '',
107
+ '><button class="query-history-item-hit ',
108
+ active ? 'is-active' : '',
109
+ '" data-action="',
110
+ escapeHtml(hitAction),
111
+ '" data-history-id="',
112
+ escapeHtml(historyId),
113
+ '"',
114
+ hitAttrs ? ` ${hitAttrs}` : '',
115
+ ' type="button">',
116
+ '<div class="query-history-item-main">',
117
+ '<span class="query-history-item-title"',
118
+ titleAttrs ? ` ${titleAttrs}` : '',
119
+ '>',
120
+ escapeHtml(title),
121
+ '</span>',
122
+ badgesMarkup,
123
+ '</div>',
124
+ '<p class="query-history-sql-preview">',
125
+ escapeHtml(preview),
126
+ '</p></button>',
127
+ '<div class="query-history-item-footer">',
128
+ footerMeta,
129
+ actionsMarkup,
130
+ '</div></article>',
131
+ ].join('');
132
+ }
@@ -1,114 +1,65 @@
1
- import { escapeHtml, formatNumber } from '../utils/format.js';
1
+ import { escapeHtml } from '../utils/format.js';
2
2
  import { renderStatusBadge } from './badges.js';
3
-
4
- function getQueryTypeTone(queryType) {
5
- if (queryType === 'select' || queryType === 'update') {
6
- return 'success';
7
- }
8
-
9
- if (queryType === 'pragma') {
10
- return 'primary';
11
- }
12
-
13
- return 'muted';
14
- }
3
+ import { renderQueryHistoryHeader, renderQueryHistorySearch } from './queryHistoryHeader.js';
4
+ import {
5
+ getQueryTypeTone,
6
+ renderQueryHistoryActionGroup,
7
+ renderQueryHistoryBadgeRow,
8
+ renderQueryHistoryIconButton,
9
+ renderQueryHistoryListItem as renderSharedQueryHistoryListItem,
10
+ renderQueryHistoryTabs,
11
+ } from './queryHistoryList.js';
15
12
 
16
13
  export function renderQueryHistoryListItem(item, activeHistoryId, selectedHistoryId) {
17
14
  const isActive = Number(activeHistoryId) === Number(item.id);
18
15
  const isSelected = Number(selectedHistoryId) === Number(item.id);
19
- const visibleTables = (item.tablesDetected ?? []).slice(0, 3);
20
- const itemClasses = [
21
- 'query-history-item',
22
- isActive ? 'is-active' : '',
23
- item.lastRun?.status === 'error' ? 'is-error' : '',
24
- ]
25
- .filter(Boolean)
26
- .join(' ');
27
- const tableMarkup = visibleTables
28
- .map(tableName =>
29
- [
30
- '<span class="border border-outline-variant/20 bg-surface-highest px-2 py-1">',
31
- escapeHtml(tableName),
32
- '</span>',
33
- ].join(''),
34
- )
35
- .join('');
16
+ const badgesMarkup = renderQueryHistoryBadgeRow(
17
+ [
18
+ renderStatusBadge(item.queryType, getQueryTypeTone(item.queryType)),
19
+ item.isSaved ? renderStatusBadge('saved', 'primary') : '',
20
+ item.isDestructive ? renderStatusBadge('destructive', 'warning') : '',
21
+ ].join(''),
22
+ );
23
+ const actionsMarkup = renderQueryHistoryActionGroup([
24
+ renderQueryHistoryIconButton({
25
+ action: 'open-query-history',
26
+ historyId: item.id,
27
+ icon: 'edit_note',
28
+ title: 'Open in editor',
29
+ }),
30
+ renderQueryHistoryIconButton({
31
+ action: 'run-query-history',
32
+ historyId: item.id,
33
+ icon: 'play_arrow',
34
+ title: 'Run query',
35
+ }),
36
+ renderQueryHistoryIconButton({
37
+ action: 'select-query-history-item',
38
+ historyId: item.id,
39
+ icon: 'info',
40
+ title: 'Open query detail',
41
+ active: isSelected,
42
+ }),
43
+ renderQueryHistoryIconButton({
44
+ action: 'toggle-query-history-saved',
45
+ historyId: item.id,
46
+ icon: item.isSaved ? 'bookmark' : 'bookmark_add',
47
+ title: item.isSaved ? 'Remove from saved' : 'Save query',
48
+ active: item.isSaved,
49
+ nextValue: item.isSaved ? 'false' : 'true',
50
+ }),
51
+ ]);
36
52
 
37
- return [
38
- '<article class="',
39
- itemClasses,
40
- '"><button class="query-history-item-hit ',
41
- isActive ? 'is-active' : '',
42
- '" data-action="select-query-history-item" data-history-id="',
43
- escapeHtml(item.id),
44
- '" type="button">',
45
- '<div class="flex flex-wrap items-center gap-2"><span class="truncate font-headline text-sm font-bold uppercase tracking-tight text-on-surface w-full">',
46
- escapeHtml(item.displayTitle),
47
- '</span>',
48
- renderStatusBadge(item.queryType, getQueryTypeTone(item.queryType)),
49
- item.isSaved ? renderStatusBadge('saved', 'primary') : '',
50
- item.isDestructive ? renderStatusBadge('destructive', 'warning') : '',
51
- '</div>',
52
- '<p class="query-history-sql-preview mt-2 text-left font-mono text-xs leading-5 text-on-surface-variant/75">',
53
- escapeHtml(item.previewSql),
54
- '</p></button>',
55
- '<div class="flex items-center justify-between gap-3 border-t border-outline-variant/10 px-3 pb-3 pt-2">',
56
- '<div class="min-w-0 flex flex-wrap gap-2 text-[10px] font-mono uppercase tracking-[0.14em] text-on-surface-variant/55">',
57
- tableMarkup,
58
- '</div><div class="flex items-center gap-1">',
59
- '<button class="query-history-icon-button" data-action="open-query-history" data-history-id="',
60
- escapeHtml(item.id),
61
- '" title="Open in editor" type="button"><span class="material-symbols-outlined text-[18px]">edit_note</span></button>',
62
- '<button class="query-history-icon-button" data-action="run-query-history" data-history-id="',
63
- escapeHtml(item.id),
64
- '" title="Run query" type="button"><span class="material-symbols-outlined text-[18px]">play_arrow</span></button>',
65
- '<button class="query-history-icon-button ',
66
- item.isSaved ? 'is-active' : '',
67
- '" data-action="toggle-query-history-saved" data-history-id="',
68
- escapeHtml(item.id),
69
- '" data-next-value="',
70
- item.isSaved ? 'false' : 'true',
71
- '" title="',
72
- item.isSaved ? 'Remove from saved' : 'Save query',
73
- '" type="button"><span class="material-symbols-outlined text-[18px]">',
74
- item.isSaved ? 'bookmark' : 'bookmark_add',
75
- '</span></button>',
76
- '<button class="query-history-icon-button ',
77
- isSelected ? 'is-active' : '',
78
- '" data-action="select-query-history-item" data-history-id="',
79
- escapeHtml(item.id),
80
- '" title="Open query detail" type="button"><span class="material-symbols-outlined text-[18px]">info</span></button>',
81
- '</div></div></article>',
82
- ].join('');
83
- }
84
-
85
- function renderQueryHistoryTabs(activeTab, historyTotal) {
86
- const tabs = [
87
- { id: 'recent', label: 'Recent' },
88
- { id: 'saved', label: 'Saved' },
89
- { id: 'unsaved', label: 'Unsaved' },
90
- { id: 'failed', label: 'Failed' },
91
- ];
92
-
93
- return [
94
- '<div class="flex items-center gap-2">',
95
- tabs
96
- .map(tab =>
97
- [
98
- '<button class="query-history-tab ',
99
- activeTab === tab.id ? 'is-active' : '',
100
- '" data-action="set-query-history-tab" data-tab="',
101
- tab.id,
102
- '" type="button">',
103
- escapeHtml(tab.label),
104
- '</button>',
105
- ].join(''),
106
- )
107
- .join(''),
108
- '<span class="ml-auto text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/50">',
109
- escapeHtml(formatNumber(historyTotal)),
110
- '</span></div>',
111
- ].join('');
53
+ return renderSharedQueryHistoryListItem({
54
+ title: item.displayTitle,
55
+ preview: item.previewSql,
56
+ historyId: item.id,
57
+ active: isActive,
58
+ error: item.lastRun?.status === 'error',
59
+ hitAction: 'select-query-history-item',
60
+ badgesMarkup,
61
+ actionsMarkup,
62
+ });
112
63
  }
113
64
 
114
65
  export function renderQueryHistoryPanel({
@@ -126,38 +77,23 @@ export function renderQueryHistoryPanel({
126
77
  }) {
127
78
  return `
128
79
  <aside class="query-history-panel border-l border-outline-variant/10 bg-surface-container-lowest">
129
- <div class="border-b border-outline-variant/10 px-4 py-4">
130
- <div class="flex items-center justify-between gap-3">
131
- <div class="flex items-center gap-2">
132
- <span class="material-symbols-outlined text-[18px] text-primary-container">history</span>
133
- <span class="font-headline text-xs font-black uppercase tracking-[0.18em] text-primary-container">
134
- Query History
135
- </span>
136
- </div>
137
- <button
138
- class="query-history-icon-button"
139
- data-action="toggle-query-history-panel"
140
- data-next-value="false"
141
- title="Hide query history"
142
- type="button"
143
- >
144
- <span class="material-symbols-outlined text-[18px]">close</span>
145
- </button>
146
- </div>
147
- <div class="mt-4">${renderQueryHistoryTabs(activeTab, total)}</div>
148
- <label class="mt-4 block">
149
- <span class="sr-only">Search query history</span>
150
- <input
151
- class="control-input w-full border border-outline-variant/20 bg-surface-container text-sm text-on-surface outline-none placeholder:text-on-surface-variant/35 focus:border-primary-container"
152
- data-bind="query-history-search"
153
- placeholder="Search SQL, titles, notes..."
154
- type="search"
155
- value="${escapeHtml(search)}"
156
- />
157
- </label>
80
+ <div class="query-history-panel__header">
81
+ ${renderQueryHistoryHeader()}
82
+ ${renderQueryHistoryTabs({
83
+ tabs: [
84
+ { id: 'recent', label: 'Recent' },
85
+ { id: 'saved', label: 'Saved' },
86
+ { id: 'unsaved', label: 'Unsaved' },
87
+ { id: 'failed', label: 'Failed' },
88
+ ],
89
+ activeTab,
90
+ action: 'set-query-history-tab',
91
+ count: total,
92
+ })}
93
+ ${renderQueryHistorySearch({ value: search })}
158
94
  </div>
159
95
  <div
160
- class="custom-scrollbar min-h-0 flex-1 overflow-auto px-3 py-3"
96
+ class="query-history-list-scroll custom-scrollbar"
161
97
  data-query-history-committed-search="${escapeHtml(committedSearch)}"
162
98
  data-query-history-loading-more="${loadingMore ? 'true' : 'false'}"
163
99
  data-query-history-search="${escapeHtml(search)}"
@@ -188,7 +124,7 @@ export function renderQueryHistoryPanel({
188
124
  `
189
125
  : ''
190
126
  }
191
- <div class="space-y-3">
127
+ <div class="query-history-list">
192
128
  ${items.map(item => renderQueryHistoryListItem(item, activeHistoryId, selectedHistoryId)).join('')}
193
129
  </div>
194
130
  ${