sqlite-hub 0.12.0 → 0.17.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 (64) hide show
  1. package/README.md +118 -23
  2. package/bin/sqlite-hub.js +1041 -224
  3. package/frontend/index.html +1 -0
  4. package/frontend/js/api.js +32 -2
  5. package/frontend/js/app.js +989 -13
  6. package/frontend/js/components/modal.js +644 -15
  7. package/frontend/js/components/pageHeader.js +14 -16
  8. package/frontend/js/components/queryEditor.js +9 -1
  9. package/frontend/js/components/queryHistoryPanel.js +126 -131
  10. package/frontend/js/components/queryResults.js +79 -17
  11. package/frontend/js/components/rowEditorPanel.js +204 -10
  12. package/frontend/js/components/sidebar.js +102 -18
  13. package/frontend/js/components/tableDesignerEditor.js +69 -11
  14. package/frontend/js/router.js +8 -0
  15. package/frontend/js/store.js +868 -9
  16. package/frontend/js/utils/copyColumnExport.js +117 -0
  17. package/frontend/js/utils/exportFilenames.js +32 -0
  18. package/frontend/js/utils/filePathPreview.js +315 -0
  19. package/frontend/js/utils/format.js +1 -1
  20. package/frontend/js/utils/markdownDocuments.js +248 -0
  21. package/frontend/js/utils/rowEditorJson.js +65 -0
  22. package/frontend/js/utils/sqlFormatter.js +691 -0
  23. package/frontend/js/utils/tableDesigner.js +178 -6
  24. package/frontend/js/utils/textCellStats.js +20 -0
  25. package/frontend/js/utils/timestampPreview.js +264 -0
  26. package/frontend/js/views/charts.js +3 -1
  27. package/frontend/js/views/data.js +34 -2
  28. package/frontend/js/views/documents.js +300 -0
  29. package/frontend/js/views/editor.js +48 -1
  30. package/frontend/js/views/settings.js +39 -6
  31. package/frontend/js/views/structure.js +154 -212
  32. package/frontend/styles/base.css +6 -0
  33. package/frontend/styles/components.css +476 -2
  34. package/frontend/styles/structure-graph.css +0 -3
  35. package/frontend/styles/tailwind.generated.css +1 -1
  36. package/frontend/styles/tokens.css +1 -1
  37. package/frontend/styles/views.css +422 -0
  38. package/package.json +3 -3
  39. package/server/routes/documents.js +163 -0
  40. package/server/routes/settings.js +22 -6
  41. package/server/server.js +6 -0
  42. package/server/services/sqlite/introspection.js +10 -0
  43. package/server/services/sqlite/sqlExecutor.js +29 -0
  44. package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
  45. package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
  46. package/server/services/sqlite/tableDesigner/validation.js +60 -2
  47. package/server/services/sqlite/tableDesignerService.js +1 -1
  48. package/server/services/storage/appStateStore.js +313 -0
  49. package/tests/check-constraint-options.test.js +14 -0
  50. package/tests/cli-args.test.js +100 -0
  51. package/tests/copy-column-modal.test.js +83 -0
  52. package/tests/database-documents.test.js +85 -0
  53. package/tests/export-filenames.test.js +34 -0
  54. package/tests/file-path-preview.test.js +165 -0
  55. package/tests/markdown-documents.test.js +79 -0
  56. package/tests/row-editor-json.test.js +82 -0
  57. package/tests/row-editor-timestamp-preview.test.js +192 -0
  58. package/tests/settings-metadata.test.js +16 -0
  59. package/tests/settings-view.test.js +32 -0
  60. package/tests/sql-formatter.test.js +173 -0
  61. package/tests/sql-highlight.test.js +38 -0
  62. package/tests/table-designer-v2-unique-constraints.test.js +78 -0
  63. package/tests/text-cell-stats.test.js +38 -0
  64. package/fill.js +0 -526
@@ -0,0 +1,300 @@
1
+ import { escapeHtml, formatCompactDateTime, formatNumber } from '../utils/format.js';
2
+ import { renderMarkdownPreview } from '../utils/markdownDocuments.js';
3
+
4
+ function renderMissingDatabase() {
5
+ return `
6
+ <section class="view-surface">
7
+ <div class="view-frame flex min-h-full items-center justify-center">
8
+ <div class="text-center">
9
+ <span class="material-symbols-outlined mb-3 text-5xl text-on-surface-variant/25">database_off</span>
10
+ <p class="font-headline text-xl font-black uppercase tracking-tight text-primary-container">
11
+ No Active SQLite Database
12
+ </p>
13
+ <p class="mt-3 max-w-xl text-sm leading-7 text-on-surface-variant/65">
14
+ Select a database before opening its document folder.
15
+ </p>
16
+ </div>
17
+ </div>
18
+ </section>
19
+ `;
20
+ }
21
+
22
+ function renderDocumentListItem(item, selectedId) {
23
+ const isSelected = String(item.id) === String(selectedId);
24
+
25
+ return `
26
+ <button
27
+ class="documents-list-item ${isSelected ? 'is-selected' : ''}"
28
+ data-action="select-document"
29
+ data-document-id="${escapeHtml(item.id)}"
30
+ type="button"
31
+ >
32
+ <span class="material-symbols-outlined documents-list-item__icon">description</span>
33
+ <span class="documents-list-item__body">
34
+ <span class="documents-list-item__title" title="${escapeHtml(item.filename)}">
35
+ ${escapeHtml(item.filename)}
36
+ </span>
37
+ <span class="documents-list-item__meta">
38
+ ${escapeHtml(formatCompactDateTime(item.updatedAt))} // ${formatNumber(item.contentLength ?? 0)} chars
39
+ </span>
40
+ </span>
41
+ </button>
42
+ `;
43
+ }
44
+
45
+ function renderDocumentsSidebar(documents) {
46
+ const items = documents.items ?? [];
47
+
48
+ return `
49
+ <aside class="documents-view__sidebar">
50
+ <div class="documents-view__sidebar-header">
51
+ <div>
52
+ <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/50">
53
+ Documents
54
+ </div>
55
+ <div class="mt-1 font-mono text-xs text-primary-container">${formatNumber(items.length)} files</div>
56
+ </div>
57
+ <button
58
+ class="icon-button"
59
+ data-action="create-document"
60
+ title="New document"
61
+ type="button"
62
+ ${documents.saving ? 'disabled aria-disabled="true"' : ''}
63
+ >
64
+ <span class="material-symbols-outlined">add</span>
65
+ </button>
66
+ </div>
67
+ <div class="documents-view__sidebar-body custom-scrollbar">
68
+ ${
69
+ items.length
70
+ ? items.map(item => renderDocumentListItem(item, documents.selectedId)).join('')
71
+ : `<div class="documents-list-empty">No documents yet</div>`
72
+ }
73
+ </div>
74
+ </aside>
75
+ `;
76
+ }
77
+
78
+ function renderDocumentToolbar(documents) {
79
+ const disabled = documents.saving || documents.detailLoading || !documents.selectedId;
80
+
81
+ return ` <label class="documents-filename-field">
82
+ <input
83
+ class="control-input"
84
+ data-bind="document-field"
85
+ data-field="filename"
86
+ name="filename"
87
+ spellcheck="false"
88
+ type="text"
89
+ value="${escapeHtml(documents.draftFilename)}"
90
+ ${!documents.selectedId ? 'disabled' : ''}
91
+ />
92
+ </label>
93
+ <div class="documents-toolbar">
94
+
95
+
96
+
97
+ <div class="documents-toolbar__actions">
98
+ <button
99
+ class="standard-button"
100
+ data-action="toggle-document-pane"
101
+ data-pane="editor"
102
+ type="button"
103
+ >
104
+ <span class="material-symbols-outlined">${documents.editorVisible ? 'visibility_off' : 'edit_note'}</span>
105
+ ${documents.editorVisible ? 'Hide Editor' : 'Show Editor'}
106
+ </button>
107
+ <button
108
+ class="standard-button"
109
+ data-action="toggle-document-pane"
110
+ data-pane="preview"
111
+ type="button"
112
+ >
113
+ <span class="material-symbols-outlined">${documents.previewVisible ? 'visibility_off' : 'visibility'}</span>
114
+ ${documents.previewVisible ? 'Hide Preview' : 'Show Preview'}
115
+ </button>
116
+ <input
117
+ accept=".md,.markdown,text/markdown,text/plain"
118
+ data-bind="document-import-file"
119
+ hidden
120
+ type="file"
121
+ />
122
+ </div>
123
+ <div class="documents-toolbar__actions">
124
+ <button
125
+ class="standard-button"
126
+ data-action="open-document-insert-table-modal"
127
+ type="button"
128
+ ${disabled ? 'disabled aria-disabled="true"' : ''}
129
+ >
130
+ <span class="material-symbols-outlined">table_chart</span>
131
+ Insert Table
132
+ </button>
133
+ <button
134
+ class="standard-button"
135
+ data-action="open-document-insert-note-modal"
136
+ type="button"
137
+ ${disabled ? 'disabled aria-disabled="true"' : ''}
138
+ >
139
+ <span class="material-symbols-outlined">note_add</span>
140
+ Insert Note
141
+ </button>
142
+ <button
143
+ class="standard-button"
144
+ data-action="export-document-markdown"
145
+ type="button"
146
+ ${disabled ? 'disabled aria-disabled="true"' : ''}
147
+ >
148
+ <span class="material-symbols-outlined">download</span>
149
+ Export .md
150
+ </button>
151
+ <button
152
+ class="standard-button"
153
+ data-action="import-document-markdown"
154
+ type="button"
155
+ ${documents.saving ? 'disabled aria-disabled="true"' : ''}
156
+ >
157
+ <span class="material-symbols-outlined">upload_file</span>
158
+ Import .md
159
+ </button>
160
+ </div>
161
+ <div class="documents-toolbar__actions">
162
+ <button
163
+ class="standard-button"
164
+ data-action="delete-document"
165
+ type="button"
166
+ ${disabled || documents.deleting ? 'disabled aria-disabled="true"' : ''}
167
+ >
168
+ <span class="material-symbols-outlined">delete</span>
169
+ Delete
170
+ </button>
171
+ <button
172
+ class="signature-button"
173
+ data-action="save-document"
174
+ type="button"
175
+ ${disabled || !documents.dirty ? 'disabled aria-disabled="true"' : ''}
176
+ >
177
+ <span class="material-symbols-outlined">save</span>
178
+ ${documents.saving ? 'Saving...' : 'Save'}
179
+ </button>
180
+ </div>
181
+ </div>
182
+ `;
183
+ }
184
+
185
+ function renderDocumentEditor(documents) {
186
+ if (!documents.editorVisible) {
187
+ return '';
188
+ }
189
+
190
+ return `
191
+ <section class="documents-pane documents-pane--editor">
192
+ <div class="documents-pane__header">
193
+ <span>Editor</span>
194
+ <span>${formatNumber(documents.draftContent.length)} chars</span>
195
+ </div>
196
+ <textarea
197
+ class="documents-editor-input custom-scrollbar"
198
+ data-bind="document-field"
199
+ data-field="content"
200
+ name="content"
201
+ spellcheck="true"
202
+ ${!documents.selectedId ? 'disabled' : ''}
203
+ >${escapeHtml(documents.draftContent)}</textarea>
204
+ </section>
205
+ `;
206
+ }
207
+
208
+ function renderDocumentPreview(documents) {
209
+ if (!documents.previewVisible) {
210
+ return '';
211
+ }
212
+
213
+ return `
214
+ <section class="documents-pane documents-pane--preview">
215
+ <div class="documents-pane__header">
216
+ <span>Preview</span>
217
+ <span>${escapeHtml(documents.dirty ? 'unsaved' : 'saved')}</span>
218
+ </div>
219
+ <div class="document-markdown-preview custom-scrollbar" data-document-preview>
220
+ ${renderMarkdownPreview(documents.draftContent)}
221
+ </div>
222
+ </section>
223
+ `;
224
+ }
225
+
226
+ function renderEmptyDocumentsState(documents) {
227
+ return `
228
+ <div class="documents-empty-state">
229
+ <span class="material-symbols-outlined">description</span>
230
+ <p class="font-headline text-2xl font-black uppercase tracking-tight text-primary-container">
231
+ No Documents
232
+ </p>
233
+ <button
234
+ class="signature-button mt-4"
235
+ data-action="create-document"
236
+ type="button"
237
+ ${documents.saving ? 'disabled aria-disabled="true"' : ''}
238
+ >
239
+ <span class="material-symbols-outlined">add</span>
240
+ New Document
241
+ </button>
242
+ </div>
243
+ `;
244
+ }
245
+
246
+ function renderDocumentDetail(documents) {
247
+ if (documents.loading && !documents.items.length) {
248
+ return `
249
+ <main class="documents-view__detail">
250
+ <div class="documents-empty-state">
251
+ <span class="material-symbols-outlined">sync</span>
252
+ <p class="font-headline text-2xl font-black uppercase tracking-tight text-primary-container">
253
+ Loading Documents
254
+ </p>
255
+ </div>
256
+ </main>
257
+ `;
258
+ }
259
+
260
+ if (!documents.selectedId) {
261
+ return `
262
+ <main class="documents-view__detail">
263
+ ${renderEmptyDocumentsState(documents)}
264
+ </main>
265
+ `;
266
+ }
267
+
268
+ const paneCount = Number(documents.editorVisible) + Number(documents.previewVisible);
269
+
270
+ return `
271
+ <main class="documents-view__detail">
272
+ ${renderDocumentToolbar(documents)}
273
+ ${documents.saveError ? `<div class="documents-error">${escapeHtml(documents.saveError.message)}</div>` : ''}
274
+ <div class="documents-workspace ${paneCount > 1 ? 'documents-workspace--split' : ''}">
275
+ ${renderDocumentEditor(documents)}
276
+ ${renderDocumentPreview(documents)}
277
+ </div>
278
+ </main>
279
+ `;
280
+ }
281
+
282
+ export function renderDocumentsView(state) {
283
+ if (!state.connections.active) {
284
+ return {
285
+ main: renderMissingDatabase(),
286
+ panel: '',
287
+ };
288
+ }
289
+
290
+ const documents = state.documents;
291
+
292
+ return {
293
+ main: `<div class="documents-view">
294
+ ${renderDocumentsSidebar(documents)}
295
+ ${renderDocumentDetail(documents)}
296
+ </div>
297
+ </div>`,
298
+ panel: '',
299
+ };
300
+ }
@@ -143,6 +143,35 @@ function getUniqueResultColumns(columns = []) {
143
143
  return uniqueColumns;
144
144
  }
145
145
 
146
+ function getEditorColumnTypeBadge(column) {
147
+ if (column.sourceColumn === 'rowid') {
148
+ return 'ROWID';
149
+ }
150
+
151
+ return String(column.declaredType || column.affinity || 'BLOB')
152
+ .trim()
153
+ .toUpperCase();
154
+ }
155
+
156
+ function getEditorColumnBadges(column) {
157
+ const badges = [{ label: getEditorColumnTypeBadge(column), tone: 'type' }];
158
+
159
+ if (Number(column.primaryKeyPosition ?? 0) > 0) {
160
+ badges.push({
161
+ label: Number(column.primaryKeyPosition) > 1 ? `PK ${column.primaryKeyPosition}` : 'PK',
162
+ tone: 'primary-key',
163
+ });
164
+ } else if (column.identity && column.sourceColumn === 'rowid') {
165
+ badges.push({ label: 'ROWID', tone: 'primary-key' });
166
+ }
167
+
168
+ if (column.foreignKey) {
169
+ badges.push({ label: 'FK', tone: 'foreign-key' });
170
+ }
171
+
172
+ return badges;
173
+ }
174
+
146
175
  function renderEditorRowPanel(state) {
147
176
  const result = state.editor.result;
148
177
  const rowIndex = state.editor.selectedRowIndex;
@@ -153,6 +182,15 @@ function renderEditorRowPanel(state) {
153
182
  }
154
183
 
155
184
  const uniqueColumns = getUniqueResultColumns(result.editing?.columns ?? []);
185
+ const tableMeta =
186
+ result.editing?.tableMeta ?? {
187
+ columns: uniqueColumns.map(column => ({
188
+ name: column.sourceColumn,
189
+ primaryKeyPosition: Number(column.primaryKeyPosition ?? 0),
190
+ foreignKey: Boolean(column.foreignKey),
191
+ })),
192
+ foreignKeys: [],
193
+ };
156
194
  const editableColumns = uniqueColumns.filter(column => {
157
195
  if (column.identity || column.generated || !column.visible) {
158
196
  return false;
@@ -193,6 +231,7 @@ function renderEditorRowPanel(state) {
193
231
  return {
194
232
  name: column.sourceColumn,
195
233
  label: column.sourceColumn,
234
+ badges: getEditorColumnBadges(column),
196
235
  allowedValues: column.allowedValues ?? [],
197
236
  notNull: Boolean(column.notNull),
198
237
  value: value === null || value === undefined ? '' : String(value),
@@ -200,15 +239,21 @@ function renderEditorRowPanel(state) {
200
239
  }),
201
240
  readonlyFields: readonlyColumns.map(column => ({
202
241
  name: column.sourceColumn,
203
- label: column.sourceColumn,
242
+ label: {
243
+ label: column.sourceColumn,
244
+ badges: getEditorColumnBadges(column),
245
+ },
246
+ rawValue: row[column.resultName],
204
247
  value: formatCellValue(row[column.resultName]),
205
248
  })),
249
+ tableMeta,
206
250
  saveError: state.editor.saveError,
207
251
  saving: state.editor.saving,
208
252
  deleting: state.editor.deleting,
209
253
  deleteAction: 'delete-editor-row',
210
254
  deleteRowIndex: rowIndex,
211
255
  deleteEnabled: editingState.enabled && Boolean(row.__identity),
256
+ jsonActionsEnabled: true,
212
257
  });
213
258
  }
214
259
 
@@ -233,6 +278,8 @@ function renderResultsSurface(state, isResultsRoute) {
233
278
  editable: editingState.enabled,
234
279
  sortColumn: state.editor.resultSortColumn,
235
280
  sortDirection: state.editor.resultSortDirection,
281
+ resultScope: 'editor',
282
+ sortAction: 'sort-editor-results-column',
236
283
  })
237
284
  : renderMissingDatabase();
238
285
  }
@@ -24,6 +24,9 @@ function renderSettingsContent(state) {
24
24
  `;
25
25
  }
26
26
 
27
+ const appVersion = escapeHtml(state.settings.appVersion ?? '0.0.0');
28
+ const sqliteVersion = escapeHtml(state.settings.sqliteVersion ?? 'unknown');
29
+
27
30
  return `
28
31
  <div class="grid grid-cols-1 gap-6 xl:grid-cols-2">
29
32
  <section class="shell-section overflow-hidden">
@@ -32,17 +35,47 @@ function renderSettingsContent(state) {
32
35
  <span class="material-symbols-outlined text-xs text-primary-container">deployed_code</span>
33
36
  </div>
34
37
  <div class="space-y-5 p-6">
38
+ <div class="grid grid-cols-1 gap-5 sm:grid-cols-2">
39
+ <div>
40
+ <div class="text-[10px] font-mono uppercase tracking-[0.2em] text-on-surface-variant/60">
41
+ Current_Version
42
+ </div>
43
+ <div class="mt-2 font-headline text-4xl font-black uppercase tracking-tight text-primary-container">
44
+ v${appVersion}
45
+ </div>
46
+ </div>
47
+ <div>
48
+ <div class="text-[10px] font-mono uppercase tracking-[0.2em] text-on-surface-variant/60">
49
+ SQLite_Runtime
50
+ </div>
51
+ <div class="mt-2 font-headline text-4xl font-black uppercase tracking-tight text-primary-container">
52
+ ${sqliteVersion}
53
+ </div>
54
+ </div>
55
+ </div>
56
+ <div class="border-t border-outline-variant/10 py-4 text-sm leading-6 text-on-surface-variant">
57
+ SQL functions and syntax are evaluated with this SQLite runtime.
58
+ </div>
59
+ </div>
60
+ </section>
61
+
62
+ <section class="shell-section overflow-hidden">
63
+ <div class="flex items-center justify-between border-b border-outline-variant/10 bg-surface-container-highest px-4 py-2">
64
+ <span class="text-[10px] font-bold uppercase tracking-[0.25em]">CLI</span>
65
+ <span class="material-symbols-outlined text-xs text-primary-container">terminal</span>
66
+ </div>
67
+ <div class="space-y-4 p-6">
35
68
  <div>
36
69
  <div class="text-[10px] font-mono uppercase tracking-[0.2em] text-on-surface-variant/60">
37
- Current_Version
38
- </div>
39
- <div class="mt-2 font-headline text-4xl font-black uppercase tracking-tight text-primary-container">
40
- v${escapeHtml(state.settings.appVersion ?? '0.0.0')}
70
+ Custom_Port
41
71
  </div>
42
- <div class="mt-2 text-sm text-on-surface-variant/70">
43
- Read directly from <code>package.json</code>.
72
+ <div class="mt-3 border border-outline-variant/10 bg-surface-container-high px-4 py-3 font-mono text-sm text-primary-container">
73
+ sqlite-hub --port:PORT
44
74
  </div>
45
75
  </div>
76
+ <div class="text-sm leading-6 text-on-surface-variant">
77
+ Start SQLite Hub with a different local port when the default port is already in use.
78
+ </div>
46
79
  </div>
47
80
  </section>
48
81