sqlite-hub 0.9.3 → 0.9.6

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 (48) hide show
  1. package/.github/workflows/ci.yml +36 -0
  2. package/README.md +2 -2
  3. package/bin/sqlite-hub.js +1 -1
  4. package/frontend/index.html +2 -158
  5. package/frontend/js/app.js +41 -22
  6. package/frontend/js/components/connectionCard.js +62 -87
  7. package/frontend/js/components/emptyState.js +20 -23
  8. package/frontend/js/components/modal.js +145 -195
  9. package/frontend/js/components/pageHeader.js +1 -1
  10. package/frontend/js/components/queryEditor.js +16 -30
  11. package/frontend/js/components/queryHistoryDetail.js +93 -164
  12. package/frontend/js/components/queryHistoryPanel.js +81 -99
  13. package/frontend/js/components/queryResults.js +3 -1
  14. package/frontend/js/components/rowEditorPanel.js +28 -31
  15. package/frontend/js/components/structureGraph.js +10 -9
  16. package/frontend/js/components/tableDesignerEditor.js +91 -116
  17. package/frontend/js/store.js +39 -3
  18. package/frontend/js/utils/dom.js +28 -0
  19. package/frontend/js/utils/tableDesigner.js +8 -2
  20. package/frontend/js/views/charts.js +23 -43
  21. package/frontend/js/views/data.js +116 -132
  22. package/frontend/js/views/mediaTagging.js +131 -164
  23. package/frontend/js/views/structure.js +52 -48
  24. package/frontend/styles/tailwind.css +80 -0
  25. package/frontend/styles/tailwind.generated.css +2 -0
  26. package/frontend/styles/tokens.css +3 -3
  27. package/package.json +19 -5
  28. package/server/routes/mediaTagging.js +2 -10
  29. package/server/routes/sql.js +35 -10
  30. package/server/server.js +24 -0
  31. package/server/services/sqlite/dataBrowserService.js +25 -5
  32. package/server/services/sqlite/exportService.js +4 -2
  33. package/server/services/sqlite/introspection.js +2 -2
  34. package/server/services/sqlite/mediaTaggingService.js +166 -53
  35. package/server/services/sqlite/structureService.js +2 -2
  36. package/server/services/sqlite/tableDesigner/sql.js +19 -3
  37. package/server/services/storage/appStateStore.js +227 -87
  38. package/server/utils/appPaths.js +55 -19
  39. package/server/utils/fileValidation.js +94 -8
  40. package/tailwind.config.cjs +73 -0
  41. package/tests/security-paths.test.js +84 -0
  42. package/tests/sql-identifier-safety.test.js +66 -0
  43. package/.npmingnore +0 -4
  44. package/changelog.md +0 -84
  45. package/docs/DESIGN_GUIDELINES.md +0 -36
  46. package/scripts/publish_brew.sh +0 -466
  47. package/scripts/publish_npm.sh +0 -241
  48. package/shortkeys.md +0 -5
@@ -57,123 +57,98 @@ function getReferencedColumns(draft, catalogTables, referencesTable) {
57
57
 
58
58
  function renderColumnRow(column, draft, catalogTables) {
59
59
  const referenceColumns = getReferencedColumns(draft, catalogTables, column.referencesTable);
60
+ const typeOptions = draft.supportedTypes
61
+ .map((type) =>
62
+ [
63
+ '<option value="',
64
+ escapeHtml(type),
65
+ '" ',
66
+ type === column.type ? "selected" : "",
67
+ ">",
68
+ escapeHtml(type),
69
+ "</option>",
70
+ ].join("")
71
+ )
72
+ .join("");
73
+ const referenceTableOptions = catalogTables
74
+ .map((table) =>
75
+ [
76
+ '<option value="',
77
+ escapeHtml(table.name),
78
+ '" ',
79
+ table.name === column.referencesTable ? "selected" : "",
80
+ ">",
81
+ escapeHtml(table.name),
82
+ "</option>",
83
+ ].join("")
84
+ )
85
+ .join("");
86
+ const referenceColumnOptions = referenceColumns
87
+ .map((name) =>
88
+ [
89
+ '<option value="',
90
+ escapeHtml(name),
91
+ '" ',
92
+ name === column.referencesColumn ? "selected" : "",
93
+ ">",
94
+ escapeHtml(name),
95
+ "</option>",
96
+ ].join("")
97
+ )
98
+ .join("");
60
99
 
61
- return `
62
- <div class="table-designer-grid__row">
63
- <input
64
- class="table-designer-field"
65
- data-bind="table-designer-column-field"
66
- data-column-id="${escapeHtml(column.id)}"
67
- data-field="name"
68
- placeholder="column_name"
69
- spellcheck="false"
70
- type="text"
71
- value="${escapeHtml(column.name)}"
72
- />
73
- <select
74
- class="table-designer-field"
75
- data-bind="table-designer-column-field"
76
- data-column-id="${escapeHtml(column.id)}"
77
- data-field="type"
78
- >
79
- ${draft.supportedTypes
80
- .map(
81
- (type) => `
82
- <option value="${escapeHtml(type)}" ${type === column.type ? "selected" : ""}>
83
- ${escapeHtml(type)}
84
- </option>
85
- `
86
- )
87
- .join("")}
88
- </select>
89
- <label class="standard-checkbox table-designer-check table-designer-checkbox-override">
90
- <input
91
- data-bind="table-designer-column-flag"
92
- data-column-id="${escapeHtml(column.id)}"
93
- data-field="notNull"
94
- type="checkbox"
95
- ${column.notNull ? "checked" : ""}
96
- />
97
- <span>Not null</span>
98
- </label>
99
- <label class="standard-checkbox table-designer-check table-designer-checkbox-override">
100
- <input
101
- data-bind="table-designer-column-flag"
102
- data-column-id="${escapeHtml(column.id)}"
103
- data-field="unique"
104
- type="checkbox"
105
- ${column.unique ? "checked" : ""}
106
- />
107
- <span>Unique</span>
108
- </label>
109
- <label class="standard-checkbox table-designer-check table-designer-checkbox-override">
110
- <input
111
- data-bind="table-designer-column-flag"
112
- data-column-id="${escapeHtml(column.id)}"
113
- data-field="primaryKey"
114
- type="checkbox"
115
- ${column.primaryKey ? "checked" : ""}
116
- />
117
- <span>PK</span>
118
- </label>
119
- <input
120
- class="table-designer-field"
121
- data-bind="table-designer-column-field"
122
- data-column-id="${escapeHtml(column.id)}"
123
- data-field="defaultValue"
124
- placeholder="SQL default"
125
- spellcheck="false"
126
- type="text"
127
- value="${escapeHtml(column.defaultValue)}"
128
- />
129
- <select
130
- class="table-designer-field"
131
- data-bind="table-designer-column-field"
132
- data-column-id="${escapeHtml(column.id)}"
133
- data-field="referencesTable"
134
- >
135
- <option value="">No FK table</option>
136
- ${catalogTables
137
- .map(
138
- (table) => `
139
- <option
140
- value="${escapeHtml(table.name)}"
141
- ${table.name === column.referencesTable ? "selected" : ""}
142
- >
143
- ${escapeHtml(table.name)}
144
- </option>
145
- `
146
- )
147
- .join("")}
148
- </select>
149
- <select
150
- class="table-designer-field"
151
- data-bind="table-designer-column-field"
152
- data-column-id="${escapeHtml(column.id)}"
153
- data-field="referencesColumn"
154
- >
155
- <option value="">No FK column</option>
156
- ${referenceColumns
157
- .map(
158
- (name) => `
159
- <option value="${escapeHtml(name)}" ${name === column.referencesColumn ? "selected" : ""}>
160
- ${escapeHtml(name)}
161
- </option>
162
- `
163
- )
164
- .join("")}
165
- </select>
166
- <button
167
- class="delete-button"
168
- data-action="remove-table-designer-column"
169
- data-column-id="${escapeHtml(column.id)}"
170
- type="button"
171
- >
172
- <span class="material-symbols-outlined text-base">delete</span>
173
- <span>Remove</span>
174
- </button>
175
- </div>
176
- `;
100
+ const columnId = escapeHtml(column.id);
101
+
102
+ return [
103
+ '<div class="table-designer-grid__row">',
104
+ '<input class="table-designer-field" data-bind="table-designer-column-field" data-column-id="',
105
+ columnId,
106
+ '" data-field="name" placeholder="column_name" spellcheck="false" type="text" value="',
107
+ escapeHtml(column.name),
108
+ '" />',
109
+ '<select class="table-designer-field" data-bind="table-designer-column-field" data-column-id="',
110
+ columnId,
111
+ '" data-field="type">',
112
+ typeOptions,
113
+ "</select>",
114
+ '<label class="standard-checkbox table-designer-check table-designer-checkbox-override">',
115
+ '<input data-bind="table-designer-column-flag" data-column-id="',
116
+ columnId,
117
+ '" data-field="notNull" type="checkbox" ',
118
+ column.notNull ? "checked" : "",
119
+ ' /><span>Not null</span></label>',
120
+ '<label class="standard-checkbox table-designer-check table-designer-checkbox-override">',
121
+ '<input data-bind="table-designer-column-flag" data-column-id="',
122
+ columnId,
123
+ '" data-field="unique" type="checkbox" ',
124
+ column.unique ? "checked" : "",
125
+ ' /><span>Unique</span></label>',
126
+ '<label class="standard-checkbox table-designer-check table-designer-checkbox-override">',
127
+ '<input data-bind="table-designer-column-flag" data-column-id="',
128
+ columnId,
129
+ '" data-field="primaryKey" type="checkbox" ',
130
+ column.primaryKey ? "checked" : "",
131
+ ' /><span>PK</span></label>',
132
+ '<input class="table-designer-field" data-bind="table-designer-column-field" data-column-id="',
133
+ columnId,
134
+ '" data-field="defaultValue" placeholder="SQL default" spellcheck="false" type="text" value="',
135
+ escapeHtml(column.defaultValue),
136
+ '" />',
137
+ '<select class="table-designer-field" data-bind="table-designer-column-field" data-column-id="',
138
+ columnId,
139
+ '" data-field="referencesTable"><option value="">No FK table</option>',
140
+ referenceTableOptions,
141
+ "</select>",
142
+ '<select class="table-designer-field" data-bind="table-designer-column-field" data-column-id="',
143
+ columnId,
144
+ '" data-field="referencesColumn"><option value="">No FK column</option>',
145
+ referenceColumnOptions,
146
+ "</select>",
147
+ '<button class="delete-button" data-action="remove-table-designer-column" data-column-id="',
148
+ columnId,
149
+ '" type="button"><span class="material-symbols-outlined text-base">delete</span><span>Remove</span></button>',
150
+ "</div>",
151
+ ].join("");
177
152
  }
178
153
 
179
154
  function renderColumnGrid(draft, catalogTables) {
@@ -32,6 +32,8 @@ const DEFAULT_SETTINGS = {
32
32
  const DEFAULT_DATA_PAGE_SIZE = 50;
33
33
  const DATA_PAGE_SIZES = [25, 50, 100];
34
34
  const DATA_ROW_SIZE_STORAGE_KEY = 'data_row_size';
35
+ const CHARTS_HISTORY_TAB_STORAGE_KEY = 'charts_history_tab';
36
+ const QUERY_HISTORY_TAB_STORAGE_KEY = 'query_history_tab';
35
37
  const QUERY_HISTORY_PAGE_SIZE = 30;
36
38
  const QUERY_HISTORY_RUN_LIMIT = 8;
37
39
  const CHART_HEIGHT_PRESETS = new Set(['small', 'medium', 'large']);
@@ -64,6 +66,38 @@ function storeDataPageSize(pageSize) {
64
66
  }
65
67
  }
66
68
 
69
+ function readStoredChartsHistoryTab(fallback = 'recent') {
70
+ try {
71
+ return normalizeChartsHistoryTab(globalThis.localStorage?.getItem(CHARTS_HISTORY_TAB_STORAGE_KEY) ?? fallback);
72
+ } catch {
73
+ return normalizeChartsHistoryTab(fallback);
74
+ }
75
+ }
76
+
77
+ function storeChartsHistoryTab(tab) {
78
+ try {
79
+ globalThis.localStorage?.setItem(CHARTS_HISTORY_TAB_STORAGE_KEY, normalizeChartsHistoryTab(tab));
80
+ } catch {
81
+ // Ignore unavailable browser storage; the in-memory setting still applies.
82
+ }
83
+ }
84
+
85
+ function readStoredQueryHistoryTab(fallback = 'recent') {
86
+ try {
87
+ return normalizeQueryHistoryTab(globalThis.localStorage?.getItem(QUERY_HISTORY_TAB_STORAGE_KEY) ?? fallback);
88
+ } catch {
89
+ return normalizeQueryHistoryTab(fallback);
90
+ }
91
+ }
92
+
93
+ function storeQueryHistoryTab(tab) {
94
+ try {
95
+ globalThis.localStorage?.setItem(QUERY_HISTORY_TAB_STORAGE_KEY, normalizeQueryHistoryTab(tab));
96
+ } catch {
97
+ // Ignore unavailable browser storage; the in-memory setting still applies.
98
+ }
99
+ }
100
+
67
101
  const state = {
68
102
  ready: false,
69
103
  route: { name: 'landing', path: '/', params: {} },
@@ -117,7 +151,7 @@ const state = {
117
151
  historyLoading: false,
118
152
  historyLoadingMore: false,
119
153
  historyError: null,
120
- historyTab: 'recent',
154
+ historyTab: readStoredQueryHistoryTab(),
121
155
  historySearchInput: '',
122
156
  historySearch: '',
123
157
  historyPageSize: QUERY_HISTORY_PAGE_SIZE,
@@ -146,7 +180,7 @@ const state = {
146
180
  queries: [],
147
181
  loading: false,
148
182
  error: null,
149
- historyTab: 'recent',
183
+ historyTab: readStoredChartsHistoryTab(),
150
184
  selectedHistoryId: null,
151
185
  chartHeightPreset: 'medium',
152
186
  sqlExpanded: false,
@@ -690,7 +724,7 @@ function resetChartsState() {
690
724
  state.charts.queries = [];
691
725
  state.charts.loading = false;
692
726
  state.charts.error = null;
693
- state.charts.historyTab = 'recent';
727
+ state.charts.historyTab = readStoredChartsHistoryTab(state.charts.historyTab);
694
728
  state.charts.selectedHistoryId = null;
695
729
  state.charts.chartHeightPreset = 'medium';
696
730
  state.charts.sqlExpanded = false;
@@ -2290,6 +2324,7 @@ export function setChartsHistoryTab(tab) {
2290
2324
  }
2291
2325
 
2292
2326
  state.charts.historyTab = nextTab;
2327
+ storeChartsHistoryTab(nextTab);
2293
2328
  emitChange();
2294
2329
  }
2295
2330
 
@@ -2621,6 +2656,7 @@ export async function setQueryHistoryTab(tab) {
2621
2656
  }
2622
2657
 
2623
2658
  state.editor.historyTab = normalizedTab;
2659
+ storeQueryHistoryTab(normalizedTab);
2624
2660
  state.editor.historyActiveId = null;
2625
2661
  clearQueryHistoryDetailState();
2626
2662
  emitChange();
@@ -0,0 +1,28 @@
1
+ function createRenderedMarkupFragment(contextNode, markup) {
2
+ const range = document.createRange();
3
+
4
+ range.selectNodeContents(contextNode);
5
+ return range.createContextualFragment(String(markup ?? ''));
6
+ }
7
+
8
+ // Renderers must escape user-controlled values before markup reaches this DOM boundary.
9
+ export function replaceChildrenFromRenderedMarkup(target, markup) {
10
+ if (!(target instanceof Element)) {
11
+ return false;
12
+ }
13
+
14
+ target.replaceChildren(createRenderedMarkupFragment(target, markup));
15
+ return true;
16
+ }
17
+
18
+ export function replaceElementFromRenderedMarkup(target, markup) {
19
+ if (!(target instanceof Element)) {
20
+ return false;
21
+ }
22
+
23
+ const range = document.createRange();
24
+
25
+ range.selectNode(target);
26
+ target.replaceWith(range.createContextualFragment(String(markup ?? '')));
27
+ return true;
28
+ }
@@ -713,13 +713,19 @@ function buildImportedInsertPreviewSql(draft, maxRows = 3) {
713
713
  }
714
714
 
715
715
  const columnSql = importedColumns.map((column) => quoteIdentifier(column.name)).join(", ");
716
- const insertPrefix = `INSERT INTO ${quoteIdentifier(draft.tableName)} (${columnSql}) VALUES`;
716
+ const insertPrefix = [
717
+ "INSERT INTO",
718
+ quoteIdentifier(draft.tableName),
719
+ "(",
720
+ columnSql,
721
+ ") VALUES",
722
+ ].join(" ");
717
723
  const previewStatements = draft.importedCsvRows.slice(0, maxRows).map((row) => {
718
724
  const valueSql = importedColumns
719
725
  .map((column) => formatImportedCellValueForSql(column, row[column.importedValueIndex] ?? ""))
720
726
  .join(", ");
721
727
 
722
- return `${insertPrefix} (${valueSql});`;
728
+ return [insertPrefix, "(", valueSql, ");"].join(" ");
723
729
  });
724
730
 
725
731
  if (draft.importedCsvRows.length > maxRows) {
@@ -374,49 +374,29 @@ function renderChartSurface(chart, state, analysis) {
374
374
  function renderChartCard(chart, state, analysis) {
375
375
  const sizeClass = resolveChartCardSizeClass(state);
376
376
 
377
- return `
378
- <article class="query-chart-card ${sizeClass}">
379
- <header class="query-chart-card__header">
380
- <div class="min-w-0">
381
- <div class="flex flex-wrap items-center gap-2">
382
- <h3 class="truncate font-headline text-xl font-black uppercase tracking-tight text-on-surface">
383
- ${escapeHtml(chart.name)}
384
- </h3>
385
- ${renderStatusBadge(getQueryChartTypeLabel(chart.chartType), 'primary')}
386
- </div>
387
- </div>
388
- <div class="flex flex-wrap items-center gap-2">
389
- <button
390
- class="standard-button"
391
- data-action="open-edit-query-chart-modal"
392
- data-chart-id="${escapeHtml(chart.id)}"
393
- type="button"
394
- >
395
- Edit
396
- </button>
397
- <button
398
- class="delete-button"
399
- data-action="open-delete-query-chart-modal"
400
- data-chart-id="${escapeHtml(chart.id)}"
401
- type="button"
402
- >
403
- Delete
404
- </button>
405
- <button
406
- class="standard-button"
407
- data-action="export-query-chart-png"
408
- data-chart-id="${escapeHtml(chart.id)}"
409
- type="button"
410
- >
411
- Export PNG
412
- </button>
413
- </div>
414
- </header>
415
- <div class="query-chart-card__body">
416
- ${renderChartSurface(chart, state, analysis)}
417
- </div>
418
- </article>
419
- `;
377
+ return [
378
+ '<article class="query-chart-card ',
379
+ sizeClass,
380
+ '"><header class="query-chart-card__header"><div class="min-w-0">',
381
+ '<div class="flex flex-wrap items-center gap-2">',
382
+ '<h3 class="truncate font-headline text-xl font-black uppercase tracking-tight text-on-surface">',
383
+ escapeHtml(chart.name),
384
+ "</h3>",
385
+ renderStatusBadge(getQueryChartTypeLabel(chart.chartType), 'primary'),
386
+ '</div></div><div class="flex flex-wrap items-center gap-2">',
387
+ '<button class="standard-button" data-action="open-edit-query-chart-modal" data-chart-id="',
388
+ escapeHtml(chart.id),
389
+ '" type="button">Edit</button>',
390
+ '<button class="delete-button" data-action="open-delete-query-chart-modal" data-chart-id="',
391
+ escapeHtml(chart.id),
392
+ '" type="button">Delete</button>',
393
+ '<button class="standard-button" data-action="export-query-chart-png" data-chart-id="',
394
+ escapeHtml(chart.id),
395
+ '" type="button">Export PNG</button>',
396
+ '</div></header><div class="query-chart-card__body">',
397
+ renderChartSurface(chart, state, analysis),
398
+ "</div></article>",
399
+ ].join("");
420
400
  }
421
401
 
422
402
  export function renderChartsDetail(state) {