sqlite-hub 0.7.0 → 0.9.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 (45) hide show
  1. package/changelog.md +25 -0
  2. package/docs/DESIGN_GUIDELINES.md +36 -0
  3. package/frontend/index.html +79 -0
  4. package/frontend/js/api.js +67 -0
  5. package/frontend/js/app.js +1401 -921
  6. package/frontend/js/components/connectionCard.js +3 -4
  7. package/frontend/js/components/emptyState.js +4 -4
  8. package/frontend/js/components/modal.js +239 -30
  9. package/frontend/js/components/queryEditor.js +11 -8
  10. package/frontend/js/components/queryHistoryDetail.js +27 -8
  11. package/frontend/js/components/queryHistoryPanel.js +6 -5
  12. package/frontend/js/components/rowEditorPanel.js +84 -58
  13. package/frontend/js/components/sidebar.js +76 -33
  14. package/frontend/js/components/structureGraph.js +629 -715
  15. package/frontend/js/components/tableDesignerEditor.js +9 -8
  16. package/frontend/js/components/tableDesignerSidebar.js +11 -10
  17. package/frontend/js/components/tableDesignerSqlPreview.js +61 -30
  18. package/frontend/js/lib/mediaTaggingDefaults.js +27 -0
  19. package/frontend/js/router.js +81 -71
  20. package/frontend/js/store.js +3095 -2165
  21. package/frontend/js/views/charts.js +68 -40
  22. package/frontend/js/views/connections.js +5 -17
  23. package/frontend/js/views/data.js +40 -27
  24. package/frontend/js/views/editor.js +172 -177
  25. package/frontend/js/views/mediaTagging.js +861 -0
  26. package/frontend/js/views/overview.js +149 -10
  27. package/frontend/js/views/settings.js +2 -2
  28. package/frontend/js/views/structure.js +74 -70
  29. package/frontend/js/views/tableDesigner.js +7 -2
  30. package/frontend/styles/base.css +73 -1
  31. package/frontend/styles/components.css +105 -105
  32. package/frontend/styles/structure-graph.css +19 -82
  33. package/frontend/styles/tokens.css +2 -0
  34. package/frontend/styles/views.css +823 -30
  35. package/package.json +1 -1
  36. package/server/routes/charts.js +4 -1
  37. package/server/routes/data.js +14 -0
  38. package/server/routes/mediaTagging.js +166 -0
  39. package/server/routes/sql.js +1 -0
  40. package/server/server.js +4 -0
  41. package/server/services/sqlite/dataBrowserService.js +25 -0
  42. package/server/services/sqlite/exportService.js +31 -1
  43. package/server/services/sqlite/mediaTaggingService.js +1689 -0
  44. package/server/services/sqlite/overviewService.js +68 -0
  45. package/server/services/storage/appStateStore.js +321 -2
@@ -1,19 +1,14 @@
1
- import { renderBottomTabs } from "../components/bottomTabs.js";
2
- import { renderQueryEditor } from "../components/queryEditor.js";
3
- import { renderQueryHistoryDetail } from "../components/queryHistoryDetail.js";
4
- import { renderQueryHistoryPanel } from "../components/queryHistoryPanel.js";
5
- import { renderRowEditorPanel } from "../components/rowEditorPanel.js";
6
- import { renderQueryResultsPane } from "../components/queryResults.js";
7
- import { getCurrentConnection, getQueryMessages, getQueryPerformance } from "../store.js";
8
- import {
9
- escapeHtml,
10
- formatCellValue,
11
- formatNumber,
12
- isBlobPreview,
13
- } from "../utils/format.js";
1
+ import { renderBottomTabs } from '../components/bottomTabs.js';
2
+ import { renderQueryEditor } from '../components/queryEditor.js';
3
+ import { renderQueryHistoryDetail } from '../components/queryHistoryDetail.js';
4
+ import { renderQueryHistoryPanel } from '../components/queryHistoryPanel.js';
5
+ import { renderRowEditorPanel } from '../components/rowEditorPanel.js';
6
+ import { renderQueryResultsPane } from '../components/queryResults.js';
7
+ import { getCurrentConnection, getQueryMessages, getQueryPerformance } from '../store.js';
8
+ import { escapeHtml, formatCellValue, formatNumber, isBlobPreview } from '../utils/format.js';
14
9
 
15
10
  function renderMissingDatabase() {
16
- return `
11
+ return `
17
12
  <div class="flex flex-1 flex-col items-center justify-center bg-surface-container-lowest px-8 text-center">
18
13
  <span class="material-symbols-outlined mb-3 text-5xl text-on-surface-variant/25">database_off</span>
19
14
  <p class="font-headline text-xl font-black uppercase tracking-tight text-primary-container">
@@ -27,60 +22,60 @@ function renderMissingDatabase() {
27
22
  }
28
23
 
29
24
  function renderMessagesPane(state) {
30
- const items = getQueryMessages(state);
25
+ const items = getQueryMessages(state);
31
26
 
32
- return `
27
+ return `
33
28
  <div class="custom-scrollbar h-full overflow-auto bg-surface-container-lowest px-6 py-6">
34
29
  <div class="space-y-4">
35
30
  ${items
36
- .map(
37
- (item) => `
31
+ .map(
32
+ item => `
38
33
  <div class="border border-outline-variant/10 bg-surface-container-low px-4 py-4">
39
34
  <div class="text-[10px] font-mono uppercase tracking-[0.2em] ${
40
- item.tone === "alert" ? "text-error" : "text-primary-container"
35
+ item.tone === 'alert' ? 'text-error' : 'text-primary-container'
41
36
  }">
42
37
  ${escapeHtml(item.label)}
43
38
  </div>
44
39
  <div class="mt-2 text-sm text-on-surface">${escapeHtml(item.value)}</div>
45
40
  </div>
46
- `
47
- )
48
- .join("")}
41
+ `,
42
+ )
43
+ .join('')}
49
44
  </div>
50
45
  </div>
51
46
  `;
52
47
  }
53
48
 
54
49
  function renderPerformancePane(state) {
55
- const metrics = getQueryPerformance(state);
50
+ const metrics = getQueryPerformance(state);
56
51
 
57
- return `
52
+ return `
58
53
  <div class="grid flex-1 grid-cols-1 gap-4 bg-surface-container-lowest p-6 md:grid-cols-4">
59
54
  <div class="metric-card">
60
55
  <span class="text-[10px] font-mono uppercase text-on-surface/40">Exec_Time</span>
61
56
  <span class="font-headline text-3xl font-bold text-on-surface">${escapeHtml(
62
- String(metrics.timingMs ?? 0)
57
+ String(metrics.timingMs ?? 0),
63
58
  )}ms</span>
64
59
  <span class="text-[10px] text-primary-container">Measured backend execution time</span>
65
60
  </div>
66
61
  <div class="metric-card">
67
62
  <span class="text-[10px] font-mono uppercase text-on-surface/40">Statements</span>
68
63
  <span class="font-headline text-3xl font-bold text-on-surface">${escapeHtml(
69
- formatNumber(metrics.statementCount)
64
+ formatNumber(metrics.statementCount),
70
65
  )}</span>
71
66
  <span class="text-[10px] text-on-surface/40">Split and executed by SQLite</span>
72
67
  </div>
73
68
  <div class="metric-card">
74
69
  <span class="text-[10px] font-mono uppercase text-on-surface/40">Rows_Returned</span>
75
70
  <span class="font-headline text-3xl font-bold text-on-surface">${escapeHtml(
76
- formatNumber(metrics.rowCount)
71
+ formatNumber(metrics.rowCount),
77
72
  )}</span>
78
73
  <span class="text-[10px] text-on-surface/40">Visible result set size</span>
79
74
  </div>
80
75
  <div class="metric-card metric-card--accent">
81
76
  <span class="text-[10px] font-mono uppercase text-on-surface/40">Rows_Affected</span>
82
77
  <span class="font-headline text-3xl font-bold text-on-surface">${escapeHtml(
83
- formatNumber(metrics.affectedRowCount)
78
+ formatNumber(metrics.affectedRowCount),
84
79
  )}</span>
85
80
  <span class="text-[10px] text-primary-container">INSERT / UPDATE / DELETE impact</span>
86
81
  </div>
@@ -89,144 +84,144 @@ function renderPerformancePane(state) {
89
84
  }
90
85
 
91
86
  function getResultEditingState(state) {
92
- const editing = state.editor.result?.editing;
87
+ const editing = state.editor.result?.editing;
93
88
 
94
- if (!editing) {
95
- return {
96
- enabled: false,
97
- message: "",
98
- };
99
- }
89
+ if (!editing) {
90
+ return {
91
+ enabled: false,
92
+ message: '',
93
+ };
94
+ }
100
95
 
101
- if (state.connections.active?.readOnly) {
102
- return {
103
- enabled: false,
104
- message: "The active database is opened read-only, so query result editing is disabled.",
105
- };
106
- }
96
+ if (state.connections.active?.readOnly) {
97
+ return {
98
+ enabled: false,
99
+ message: 'The active database is opened read-only, so query result editing is disabled.',
100
+ };
101
+ }
102
+
103
+ if (!editing.enabled) {
104
+ return {
105
+ enabled: false,
106
+ message: editing.reason || 'Only direct single-table SELECT results can be edited here.',
107
+ };
108
+ }
107
109
 
108
- if (!editing.enabled) {
109
110
  return {
110
- enabled: false,
111
- message: editing.reason || "Only direct single-table SELECT results can be edited here.",
111
+ enabled: true,
112
+ message: `Click a row to edit it in ${editing.tableName}.`,
112
113
  };
113
- }
114
-
115
- return {
116
- enabled: true,
117
- message: `Click a row to edit it in ${editing.tableName}.`,
118
- };
119
114
  }
120
115
 
121
116
  function getUniqueResultColumns(columns = []) {
122
- const uniqueColumns = [];
123
- const seen = new Set();
117
+ const uniqueColumns = [];
118
+ const seen = new Set();
124
119
 
125
- columns.forEach((column) => {
126
- if (!column?.sourceColumn || seen.has(column.sourceColumn)) {
127
- return;
128
- }
120
+ columns.forEach(column => {
121
+ if (!column?.sourceColumn || seen.has(column.sourceColumn)) {
122
+ return;
123
+ }
129
124
 
130
- seen.add(column.sourceColumn);
131
- uniqueColumns.push(column);
132
- });
125
+ seen.add(column.sourceColumn);
126
+ uniqueColumns.push(column);
127
+ });
133
128
 
134
- return uniqueColumns;
129
+ return uniqueColumns;
135
130
  }
136
131
 
137
132
  function renderEditorRowPanel(state) {
138
- const result = state.editor.result;
139
- const rowIndex = state.editor.selectedRowIndex;
140
- const row = typeof rowIndex === "number" ? result?.rows?.[rowIndex] ?? null : null;
133
+ const result = state.editor.result;
134
+ const rowIndex = state.editor.selectedRowIndex;
135
+ const row = typeof rowIndex === 'number' ? (result?.rows?.[rowIndex] ?? null) : null;
141
136
 
142
- if (!result || !row || typeof rowIndex !== "number") {
143
- return "";
144
- }
145
-
146
- const uniqueColumns = getUniqueResultColumns(result.editing?.columns ?? []);
147
- const editableColumns = uniqueColumns.filter((column) => {
148
- if (column.identity || column.generated || !column.visible) {
149
- return false;
137
+ if (!result || !row || typeof rowIndex !== 'number') {
138
+ return '';
150
139
  }
151
140
 
152
- const value = row[column.resultName];
153
- if (isBlobPreview(value) || (value && typeof value === "object")) {
154
- return false;
155
- }
141
+ const uniqueColumns = getUniqueResultColumns(result.editing?.columns ?? []);
142
+ const editableColumns = uniqueColumns.filter(column => {
143
+ if (column.identity || column.generated || !column.visible) {
144
+ return false;
145
+ }
156
146
 
157
- return true;
158
- });
159
- const readonlyColumns = uniqueColumns.filter((column) => {
160
- if (!column.visible) {
161
- return false;
162
- }
147
+ const value = row[column.resultName];
148
+ if (isBlobPreview(value) || (value && typeof value === 'object')) {
149
+ return false;
150
+ }
163
151
 
164
- if (column.identity || column.generated) {
165
- return true;
166
- }
152
+ return true;
153
+ });
154
+ const readonlyColumns = uniqueColumns.filter(column => {
155
+ if (!column.visible) {
156
+ return false;
157
+ }
167
158
 
168
- const value = row[column.resultName];
169
- return isBlobPreview(value) || (value && typeof value === "object");
170
- });
171
- const editingState = getResultEditingState(state);
159
+ if (column.identity || column.generated) {
160
+ return true;
161
+ }
162
+
163
+ const value = row[column.resultName];
164
+ return isBlobPreview(value) || (value && typeof value === 'object');
165
+ });
166
+ const editingState = getResultEditingState(state);
172
167
 
173
- return renderRowEditorPanel({
174
- title: result.editing?.tableName ?? "Query Result",
175
- sectionLabel: "Row Editor",
176
- subtitle: `query row ${rowIndex + 1}`,
177
- closeAction: "clear-editor-row-selection",
178
- formName: "save-editor-row",
179
- hiddenFields: [{ name: "rowIndex", value: String(rowIndex) }],
180
- disabledMessage: editingState.enabled ? "" : editingState.message,
181
- editableFields: editableColumns.map((column) => {
182
- const value = row[column.resultName];
168
+ return renderRowEditorPanel({
169
+ title: result.editing?.tableName ?? 'Query Result',
170
+ sectionLabel: 'Row Editor',
171
+ subtitle: `query row ${rowIndex + 1}`,
172
+ closeAction: 'clear-editor-row-selection',
173
+ formName: 'save-editor-row',
174
+ hiddenFields: [{ name: 'rowIndex', value: String(rowIndex) }],
175
+ disabledMessage: editingState.enabled ? '' : editingState.message,
176
+ editableFields: editableColumns.map(column => {
177
+ const value = row[column.resultName];
183
178
 
184
- return {
185
- name: column.sourceColumn,
186
- label: column.sourceColumn,
187
- value: value === null || value === undefined ? "" : String(value),
188
- };
189
- }),
190
- readonlyFields: readonlyColumns.map((column) => ({
191
- name: column.sourceColumn,
192
- label: column.sourceColumn,
193
- value: formatCellValue(row[column.resultName]),
194
- })),
195
- saveError: state.editor.saveError,
196
- saving: state.editor.saving,
197
- deleting: state.editor.deleting,
198
- deleteAction: "delete-editor-row",
199
- deleteRowIndex: rowIndex,
200
- deleteEnabled: editingState.enabled && Boolean(row.__identity),
201
- });
179
+ return {
180
+ name: column.sourceColumn,
181
+ label: column.sourceColumn,
182
+ value: value === null || value === undefined ? '' : String(value),
183
+ };
184
+ }),
185
+ readonlyFields: readonlyColumns.map(column => ({
186
+ name: column.sourceColumn,
187
+ label: column.sourceColumn,
188
+ value: formatCellValue(row[column.resultName]),
189
+ })),
190
+ saveError: state.editor.saveError,
191
+ saving: state.editor.saving,
192
+ deleting: state.editor.deleting,
193
+ deleteAction: 'delete-editor-row',
194
+ deleteRowIndex: rowIndex,
195
+ deleteEnabled: editingState.enabled && Boolean(row.__identity),
196
+ });
202
197
  }
203
198
 
204
199
  function renderResultsSurface(state, isResultsRoute) {
205
- const activeTab = state.editor.activeTab;
206
- const counts = {
207
- resultRows: state.editor.result?.rows?.length ?? 0,
208
- messages: getQueryMessages(state).length,
209
- statementCount: state.editor.result?.statementCount ?? 0,
210
- };
200
+ const activeTab = state.editor.activeTab;
201
+ const counts = {
202
+ resultRows: state.editor.result?.rows?.length ?? 0,
203
+ messages: getQueryMessages(state).length,
204
+ statementCount: state.editor.result?.statementCount ?? 0,
205
+ };
211
206
 
212
- let content = renderMessagesPane(state);
207
+ let content = renderMessagesPane(state);
213
208
 
214
- if (activeTab === "performance") {
215
- content = renderPerformancePane(state);
216
- } else if (activeTab === "results") {
217
- const editingState = getResultEditingState(state);
209
+ if (activeTab === 'performance') {
210
+ content = renderPerformancePane(state);
211
+ } else if (activeTab === 'results') {
212
+ const editingState = getResultEditingState(state);
218
213
 
219
- content = state.connections.active
220
- ? renderQueryResultsPane(state.editor.result, {
221
- selectedRowIndex: state.editor.selectedRowIndex,
222
- editable: editingState.enabled,
223
- sortColumn: state.editor.resultSortColumn,
224
- sortDirection: state.editor.resultSortDirection,
225
- })
226
- : renderMissingDatabase();
227
- }
214
+ content = state.connections.active
215
+ ? renderQueryResultsPane(state.editor.result, {
216
+ selectedRowIndex: state.editor.selectedRowIndex,
217
+ editable: editingState.enabled,
218
+ sortColumn: state.editor.resultSortColumn,
219
+ sortDirection: state.editor.resultSortDirection,
220
+ })
221
+ : renderMissingDatabase();
222
+ }
228
223
 
229
- return `
224
+ return `
230
225
  <div class="flex h-full min-h-0 flex-col border-t border-outline-variant/10 bg-surface-container-lowest">
231
226
  ${renderBottomTabs(state.editor.activeTab, counts)}
232
227
  <div class="min-h-0 flex-1">${content}</div>
@@ -235,26 +230,26 @@ function renderResultsSurface(state, isResultsRoute) {
235
230
  }
236
231
 
237
232
  export function renderEditorView(state, { isResultsRoute = false } = {}) {
238
- const connection = getCurrentConnection(state);
239
- const editorSectionClass = state.editor.editorPanelVisible ? "min-h-[27.5%]" : "flex-none";
240
- const resultsSectionClass = "flex-1";
233
+ const connection = getCurrentConnection(state);
234
+ const editorSectionClass = state.editor.editorPanelVisible ? 'min-h-[27.5%] max-h-[60%]' : 'flex-none';
235
+ const resultsSectionClass = 'flex-1';
241
236
 
242
- return {
243
- main: `
237
+ return {
238
+ main: `
244
239
  <section class="view-surface flex h-full min-h-0 flex-col overflow-hidden xl:flex-row">
245
240
  <div class="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
246
241
  <section class="${editorSectionClass} flex min-h-0 flex-col ${
247
- isResultsRoute ? "border-b-4 border-background" : ""
242
+ isResultsRoute ? 'border-b-4 border-background' : ''
248
243
  }">
249
244
  ${renderQueryEditor({
250
- query: state.editor.sqlText,
251
- executing: state.editor.executing,
252
- exporting: state.editor.exportLoading,
253
- historyLoading: state.editor.historyLoading,
254
- historyTotal: state.editor.historyTotal,
255
- editorVisible: state.editor.editorPanelVisible,
256
- historyVisible: state.editor.historyPanelVisible,
257
- title: connection?.label ?? "SQLite Query Workspace",
245
+ query: state.editor.sqlText,
246
+ executing: state.editor.executing,
247
+ exporting: state.editor.exportLoading,
248
+ historyLoading: state.editor.historyLoading,
249
+ historyTotal: state.editor.historyTotal,
250
+ editorVisible: state.editor.editorPanelVisible,
251
+ historyVisible: state.editor.historyPanelVisible,
252
+ title: connection?.label ?? 'SQLite Query Workspace',
258
253
  })}
259
254
  </section>
260
255
  <section class="${resultsSectionClass} flex min-h-0 min-w-0 flex-col overflow-hidden">
@@ -262,33 +257,33 @@ export function renderEditorView(state, { isResultsRoute = false } = {}) {
262
257
  </section>
263
258
  </div>
264
259
  ${
265
- state.editor.historyPanelVisible
266
- ? renderQueryHistoryPanel({
267
- items: state.editor.history,
268
- loading: state.editor.historyLoading,
269
- loadingMore: state.editor.historyLoadingMore,
270
- error: state.editor.historyError,
271
- activeTab: state.editor.historyTab,
272
- search: state.editor.historySearchInput,
273
- total: state.editor.historyTotal,
274
- hasMore: state.editor.historyHasMore,
275
- activeHistoryId: state.editor.historyActiveId,
276
- selectedHistoryId: state.editor.historySelectedId,
277
- })
278
- : ""
260
+ state.editor.historyPanelVisible
261
+ ? renderQueryHistoryPanel({
262
+ items: state.editor.history,
263
+ loading: state.editor.historyLoading,
264
+ loadingMore: state.editor.historyLoadingMore,
265
+ error: state.editor.historyError,
266
+ activeTab: state.editor.historyTab,
267
+ search: state.editor.historySearchInput,
268
+ total: state.editor.historyTotal,
269
+ hasMore: state.editor.historyHasMore,
270
+ activeHistoryId: state.editor.historyActiveId,
271
+ selectedHistoryId: state.editor.historySelectedId,
272
+ })
273
+ : ''
279
274
  }
280
275
  </section>
281
276
  `,
282
- panel:
283
- isResultsRoute && state.editor.selectedRowIndex !== null
284
- ? renderEditorRowPanel(state)
285
- : state.editor.historySelectedId
286
- ? renderQueryHistoryDetail({
287
- item: state.editor.historyDetail,
288
- runs: state.editor.historyRuns,
289
- loading: state.editor.historyDetailLoading,
290
- error: state.editor.historyDetailError,
291
- })
292
- : "",
293
- };
277
+ panel:
278
+ isResultsRoute && state.editor.selectedRowIndex !== null
279
+ ? renderEditorRowPanel(state)
280
+ : state.editor.historySelectedId
281
+ ? renderQueryHistoryDetail({
282
+ item: state.editor.historyDetail,
283
+ runs: state.editor.historyRuns,
284
+ loading: state.editor.historyDetailLoading,
285
+ error: state.editor.historyDetailError,
286
+ })
287
+ : '',
288
+ };
294
289
  }