sqlite-hub 0.9.3 → 0.9.4
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/.github/workflows/ci.yml +36 -0
- package/README.md +2 -2
- package/bin/sqlite-hub.js +1 -1
- package/frontend/index.html +2 -158
- package/frontend/js/app.js +5 -1
- package/frontend/js/components/connectionCard.js +62 -87
- package/frontend/js/components/emptyState.js +20 -23
- package/frontend/js/components/modal.js +145 -195
- package/frontend/js/components/pageHeader.js +1 -1
- package/frontend/js/components/queryEditor.js +16 -30
- package/frontend/js/components/queryHistoryDetail.js +93 -164
- package/frontend/js/components/queryHistoryPanel.js +81 -99
- package/frontend/js/components/queryResults.js +3 -1
- package/frontend/js/components/rowEditorPanel.js +28 -31
- package/frontend/js/components/tableDesignerEditor.js +91 -116
- package/frontend/js/store.js +39 -3
- package/frontend/js/utils/tableDesigner.js +8 -2
- package/frontend/js/views/charts.js +23 -43
- package/frontend/js/views/data.js +116 -132
- package/frontend/js/views/mediaTagging.js +131 -164
- package/frontend/js/views/structure.js +52 -48
- package/frontend/styles/tailwind.css +81 -0
- package/frontend/styles/tailwind.generated.css +1 -0
- package/frontend/styles/tokens.css +3 -3
- package/package.json +16 -3
- package/server/routes/mediaTagging.js +2 -10
- package/server/routes/sql.js +35 -10
- package/server/server.js +24 -0
- package/server/services/sqlite/dataBrowserService.js +25 -5
- package/server/services/sqlite/exportService.js +4 -2
- package/server/services/sqlite/introspection.js +2 -2
- package/server/services/sqlite/mediaTaggingService.js +166 -53
- package/server/services/sqlite/structureService.js +2 -2
- package/server/services/sqlite/tableDesigner/sql.js +19 -3
- package/server/services/storage/appStateStore.js +227 -87
- package/server/utils/appPaths.js +55 -19
- package/server/utils/fileValidation.js +94 -8
- package/tailwind.config.cjs +73 -0
- package/tests/security-paths.test.js +84 -0
- package/tests/sql-identifier-safety.test.js +66 -0
- package/.npmingnore +0 -4
- package/changelog.md +0 -84
- package/docs/DESIGN_GUIDELINES.md +0 -36
- package/scripts/publish_brew.sh +0 -466
- package/scripts/publish_npm.sh +0 -241
- 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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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) {
|
package/frontend/js/store.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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 =
|
|
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();
|
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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) {
|