sqlite-hub 0.9.13 → 0.11.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.
- package/frontend/js/api.js +16 -0
- package/frontend/js/app.js +28 -6
- package/frontend/js/components/modal.js +98 -1
- package/frontend/js/components/queryEditor.js +1 -8
- package/frontend/js/store.js +260 -18
- package/frontend/js/views/data.js +128 -69
- package/frontend/js/views/editor.js +1 -3
- package/frontend/js/views/structure.js +43 -5
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/views.css +4 -0
- package/package.json +1 -1
- package/server/routes/data.js +17 -0
- package/server/services/sqlite/dataBrowserService.js +215 -48
- package/tests/sql-identifier-safety.test.js +107 -8
|
@@ -16,8 +16,37 @@ function getSelectedRow(state) {
|
|
|
16
16
|
return state.dataBrowser.table?.rows?.[rowIndex] ?? null;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function getFilteredTables(tables, searchQuery) {
|
|
20
|
+
const normalizedSearch = String(searchQuery ?? '')
|
|
21
|
+
.trim()
|
|
22
|
+
.toLowerCase();
|
|
23
|
+
|
|
24
|
+
if (!normalizedSearch) {
|
|
25
|
+
return tables;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return tables.filter(table => table.name.toLowerCase().includes(normalizedSearch));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderDataTableSearch(state) {
|
|
32
|
+
return `
|
|
33
|
+
<label class="table-designer-sidebar__search">
|
|
34
|
+
<span class="material-symbols-outlined text-sm text-on-surface-variant/55">search</span>
|
|
35
|
+
<input
|
|
36
|
+
class="table-designer-sidebar__search-input"
|
|
37
|
+
data-bind="data-table-search"
|
|
38
|
+
placeholder="Search tables..."
|
|
39
|
+
spellcheck="false"
|
|
40
|
+
type="search"
|
|
41
|
+
value="${escapeHtml(state.dataBrowser.tableSearchQuery ?? '')}"
|
|
42
|
+
/>
|
|
43
|
+
</label>
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
|
|
19
47
|
function renderTableList(state) {
|
|
20
48
|
const tables = state.dataBrowser.tables ?? [];
|
|
49
|
+
const filteredTables = getFilteredTables(tables, state.dataBrowser.tableSearchQuery);
|
|
21
50
|
const activeName = state.dataBrowser.selectedTable;
|
|
22
51
|
|
|
23
52
|
if (state.dataBrowser.loading && !tables.length) {
|
|
@@ -39,10 +68,18 @@ function renderTableList(state) {
|
|
|
39
68
|
`;
|
|
40
69
|
}
|
|
41
70
|
|
|
71
|
+
if (!filteredTables.length) {
|
|
72
|
+
return `
|
|
73
|
+
<div class="px-6 py-6 text-sm text-on-surface-variant/55">
|
|
74
|
+
No tables match the current search.
|
|
75
|
+
</div>
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
|
|
42
79
|
return `
|
|
43
80
|
<div class="custom-scrollbar flex-1 overflow-auto px-4 py-4">
|
|
44
81
|
<div class="space-y-2">
|
|
45
|
-
${
|
|
82
|
+
${filteredTables
|
|
46
83
|
.map(
|
|
47
84
|
table => `
|
|
48
85
|
<button
|
|
@@ -203,71 +240,88 @@ function renderSortableHeader(columnName, sortColumn, sortDirection, action) {
|
|
|
203
240
|
`;
|
|
204
241
|
}
|
|
205
242
|
|
|
206
|
-
function
|
|
207
|
-
const allRows = table?.rows ?? [];
|
|
243
|
+
function getActiveFilterColumn(table, state) {
|
|
208
244
|
const availableColumns = table?.columns ?? [];
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
.toLowerCase();
|
|
212
|
-
const activeColumn = availableColumns.includes(state.dataBrowser.searchColumn)
|
|
245
|
+
|
|
246
|
+
return availableColumns.includes(state.dataBrowser.searchColumn)
|
|
213
247
|
? state.dataBrowser.searchColumn
|
|
214
248
|
: (availableColumns[0] ?? '');
|
|
249
|
+
}
|
|
215
250
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
251
|
+
function isTextFilterColumn(table, columnName) {
|
|
252
|
+
const column = (table?.columnMeta ?? []).find(item => item.name === columnName);
|
|
253
|
+
|
|
254
|
+
return String(column?.affinity ?? '').toUpperCase() === 'TEXT';
|
|
255
|
+
}
|
|
220
256
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
filteredRows: indexedRows,
|
|
225
|
-
searchQuery,
|
|
226
|
-
};
|
|
257
|
+
function getFilterOperatorLabel(operator, textColumn) {
|
|
258
|
+
if (textColumn && operator === '=') {
|
|
259
|
+
return 'contains';
|
|
227
260
|
}
|
|
228
261
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
262
|
+
if (textColumn && operator === 'equals') {
|
|
263
|
+
return 'equals';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (textColumn && operator === '!=') {
|
|
267
|
+
return 'not contains';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return operator;
|
|
236
271
|
}
|
|
237
272
|
|
|
238
|
-
function
|
|
273
|
+
function renderTableFilterBar(table, state, activeColumn) {
|
|
239
274
|
const columns = table?.columns ?? [];
|
|
240
275
|
|
|
241
276
|
if (!table || !columns.length) {
|
|
242
277
|
return '';
|
|
243
278
|
}
|
|
279
|
+
|
|
244
280
|
const columnOptions = columns
|
|
245
|
-
.map(columnName =>
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
281
|
+
.map(columnName =>
|
|
282
|
+
[
|
|
283
|
+
'<option value="',
|
|
284
|
+
escapeHtml(columnName),
|
|
285
|
+
'" ',
|
|
286
|
+
columnName === activeColumn ? 'selected' : '',
|
|
287
|
+
'>',
|
|
288
|
+
escapeHtml(columnName),
|
|
289
|
+
'</option>',
|
|
290
|
+
].join(''),
|
|
291
|
+
)
|
|
254
292
|
.join('');
|
|
293
|
+
const activeColumnIsText = isTextFilterColumn(table, activeColumn);
|
|
294
|
+
const operators = activeColumnIsText ? ['=', 'equals', '!=', '<', '>', '<=', '>='] : ['=', '!=', '<', '>', '<=', '>='];
|
|
295
|
+
const activeOperator = operators.includes(state.dataBrowser.filterOperator) ? state.dataBrowser.filterOperator : '=';
|
|
296
|
+
const operatorOptions = operators
|
|
297
|
+
.map(operator =>
|
|
298
|
+
[
|
|
299
|
+
'<option value="',
|
|
300
|
+
escapeHtml(operator),
|
|
301
|
+
'" ',
|
|
302
|
+
operator === activeOperator ? 'selected' : '',
|
|
303
|
+
'>',
|
|
304
|
+
escapeHtml(getFilterOperatorLabel(operator, activeColumnIsText)),
|
|
305
|
+
'</option>',
|
|
306
|
+
].join(''),
|
|
307
|
+
)
|
|
308
|
+
.join('');
|
|
309
|
+
const filterValue = String(state.dataBrowser.searchQuery ?? '');
|
|
255
310
|
|
|
256
311
|
return [
|
|
257
312
|
'<div class="flex flex-wrap items-center gap-3 border-b border-outline-variant/10 bg-surface-container-low px-6 py-4">',
|
|
258
|
-
'<
|
|
259
|
-
'<span class="material-symbols-outlined text-base text-on-surface-variant/55">search</span>',
|
|
260
|
-
'<input class="control-input control-input--ghost min-w-0 flex-1 text-sm text-on-surface outline-none placeholder:text-on-surface-variant/40" data-bind="data-search-query" placeholder="Filter current page..." type="search" value="',
|
|
261
|
-
escapeHtml(state.dataBrowser.searchQuery ?? ''),
|
|
262
|
-
'" /></label>',
|
|
313
|
+
'<span class="material-symbols-outlined text-base text-on-surface-variant/55">filter_alt</span>',
|
|
263
314
|
'<select class="control-select min-w-[14rem] border border-outline-variant/20 bg-surface-container-lowest font-mono text-xs tracking-[0.04em] text-on-surface outline-none" data-bind="data-search-column">',
|
|
264
315
|
columnOptions,
|
|
265
316
|
'</select>',
|
|
266
|
-
'<
|
|
267
|
-
|
|
268
|
-
'
|
|
269
|
-
|
|
270
|
-
' on
|
|
317
|
+
'<select class="control-select min-w-[7rem] border border-outline-variant/20 bg-surface-container-lowest font-mono text-xs tracking-[0.04em] text-on-surface outline-none" data-bind="data-filter-operator">',
|
|
318
|
+
operatorOptions,
|
|
319
|
+
'</select>',
|
|
320
|
+
'<label class="control-shell flex min-w-[18rem] flex-1 items-center gap-3 border border-outline-variant/20 bg-surface-container-lowest px-3">',
|
|
321
|
+
'<input class="control-input control-input--ghost min-w-0 flex-1 text-sm text-on-surface outline-none placeholder:text-on-surface-variant/40" data-bind="data-search-query" placeholder="Value..." type="search" value="',
|
|
322
|
+
escapeHtml(filterValue),
|
|
323
|
+
'" /></label>',
|
|
324
|
+
'</div>',
|
|
271
325
|
].join('');
|
|
272
326
|
}
|
|
273
327
|
|
|
@@ -300,7 +354,11 @@ function renderTableSurface(state) {
|
|
|
300
354
|
`;
|
|
301
355
|
}
|
|
302
356
|
|
|
303
|
-
const
|
|
357
|
+
const activeColumn = getActiveFilterColumn(table, state);
|
|
358
|
+
const indexedRows = (table.rows ?? []).map((row, index) => ({
|
|
359
|
+
row,
|
|
360
|
+
index,
|
|
361
|
+
}));
|
|
304
362
|
const sortColumn = state.dataBrowser.sortColumn;
|
|
305
363
|
const sortDirection = state.dataBrowser.sortDirection;
|
|
306
364
|
const columns = (table.columns ?? []).map(columnName => ({
|
|
@@ -324,44 +382,41 @@ function renderTableSurface(state) {
|
|
|
324
382
|
const pageCount = table.pageCount ?? Math.max(1, Math.ceil(totalRows / (table.limit ?? 50)));
|
|
325
383
|
const fromRow = totalRows === 0 ? 0 : (table.offset ?? 0) + 1;
|
|
326
384
|
const toRow = totalRows === 0 ? 0 : Math.min((table.offset ?? 0) + (table.rows?.length ?? 0), totalRows);
|
|
327
|
-
const pageSizes = [25, 50, 100];
|
|
328
|
-
const
|
|
329
|
-
const hasActiveSearch = Boolean(searchQuery);
|
|
385
|
+
const pageSizes = Object.freeze([25, 50, 100, 250]);
|
|
386
|
+
const hasActiveFilter = Boolean(String(state.dataBrowser.searchQuery ?? '').trim() && activeColumn);
|
|
330
387
|
const gridMarkup = renderDataGrid({
|
|
331
388
|
columns,
|
|
332
|
-
rows:
|
|
389
|
+
rows: indexedRows.map(({ row }) => row),
|
|
333
390
|
tableClass: 'min-w-full border-collapse text-left font-mono text-xs',
|
|
334
391
|
theadClass: 'sticky top-0 z-10 bg-surface-container-highest',
|
|
335
392
|
tbodyClass: 'divide-y divide-outline-variant/5',
|
|
336
|
-
getRowClass: (_,
|
|
337
|
-
const rowIndex =
|
|
393
|
+
getRowClass: (_, rowIndexOnPage) => {
|
|
394
|
+
const rowIndex = indexedRows[rowIndexOnPage]?.index ?? rowIndexOnPage;
|
|
338
395
|
|
|
339
396
|
return [
|
|
340
397
|
'data-browser-row',
|
|
341
|
-
|
|
398
|
+
rowIndexOnPage % 2 === 0 ? 'data-browser-row--even' : 'data-browser-row--odd',
|
|
342
399
|
state.dataBrowser.selectedRowIndex === rowIndex ? 'is-selected' : '',
|
|
343
400
|
'cursor-pointer transition-colors',
|
|
344
|
-
]
|
|
401
|
+
]
|
|
402
|
+
.filter(Boolean)
|
|
403
|
+
.join(' ');
|
|
345
404
|
},
|
|
346
|
-
getRowAttrs: (_,
|
|
347
|
-
const rowIndex =
|
|
405
|
+
getRowAttrs: (_, rowIndexOnPage) => {
|
|
406
|
+
const rowIndex = indexedRows[rowIndexOnPage]?.index ?? rowIndexOnPage;
|
|
348
407
|
|
|
349
408
|
return ['data-action="select-data-row" data-row-index="', rowIndex, '"'].join('');
|
|
350
409
|
},
|
|
351
410
|
});
|
|
352
411
|
const emptyMarkup = !table.rows?.length
|
|
353
|
-
?
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
'</p></div>',
|
|
360
|
-
].join('')
|
|
361
|
-
: '';
|
|
362
|
-
const visibleRowsText = hasActiveSearch
|
|
363
|
-
? [' // ', escapeHtml(formatNumber(filteredRowCount)), ' visible on this page'].join('')
|
|
412
|
+
? [
|
|
413
|
+
'<div class="flex min-h-[180px] items-center justify-center border-t border-outline-variant/10">',
|
|
414
|
+
'<p class="font-mono text-[10px] uppercase tracking-[0.22em] text-on-surface-variant/40">',
|
|
415
|
+
hasActiveFilter ? 'NO_ROWS_MATCH_FILTER' : 'TABLE_IS_EMPTY',
|
|
416
|
+
'</p></div>',
|
|
417
|
+
].join('')
|
|
364
418
|
: '';
|
|
419
|
+
const filteredRowsText = hasActiveFilter ? ' filtered' : '';
|
|
365
420
|
const pageSizeButtons = pageSizes
|
|
366
421
|
.map(pageSize =>
|
|
367
422
|
[
|
|
@@ -378,7 +433,7 @@ function renderTableSurface(state) {
|
|
|
378
433
|
|
|
379
434
|
return [
|
|
380
435
|
'<div class="flex flex-1 min-h-0 flex-col bg-surface-container-lowest">',
|
|
381
|
-
|
|
436
|
+
renderTableFilterBar(table, state, activeColumn),
|
|
382
437
|
'<div class="custom-scrollbar flex-1 overflow-auto">',
|
|
383
438
|
gridMarkup,
|
|
384
439
|
emptyMarkup,
|
|
@@ -390,8 +445,8 @@ function renderTableSurface(state) {
|
|
|
390
445
|
escapeHtml(formatNumber(toRow)),
|
|
391
446
|
' of ',
|
|
392
447
|
escapeHtml(formatNumber(totalRows)),
|
|
448
|
+
filteredRowsText,
|
|
393
449
|
' rows',
|
|
394
|
-
visibleRowsText,
|
|
395
450
|
'</div>',
|
|
396
451
|
'<div class="flex flex-wrap items-center gap-4"><div class="flex items-center gap-2">',
|
|
397
452
|
'<span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">rows</span>',
|
|
@@ -433,7 +488,10 @@ export function renderDataRowEditorPanel(state) {
|
|
|
433
488
|
(foreignKey.mappings ?? []).map(mapping => String(mapping.from ?? '').trim()).filter(Boolean),
|
|
434
489
|
),
|
|
435
490
|
);
|
|
436
|
-
const getColumnTypeBadge = column =>
|
|
491
|
+
const getColumnTypeBadge = column =>
|
|
492
|
+
String(column.declaredType || column.affinity || 'BLOB')
|
|
493
|
+
.trim()
|
|
494
|
+
.toUpperCase();
|
|
437
495
|
const getColumnNumberInputMeta = column => {
|
|
438
496
|
const affinity = String(column.affinity ?? '').toUpperCase();
|
|
439
497
|
|
|
@@ -556,6 +614,7 @@ export function renderDataView(state) {
|
|
|
556
614
|
total ${escapeHtml(formatNumber(state.dataBrowser.tables.length))}
|
|
557
615
|
</div>
|
|
558
616
|
</div>
|
|
617
|
+
${renderDataTableSearch(state)}
|
|
559
618
|
${renderTableList(state)}
|
|
560
619
|
</aside>
|
|
561
620
|
`
|
|
@@ -4,7 +4,7 @@ import { renderQueryHistoryDetail } from '../components/queryHistoryDetail.js';
|
|
|
4
4
|
import { renderQueryHistoryPanel } from '../components/queryHistoryPanel.js';
|
|
5
5
|
import { renderRowEditorPanel } from '../components/rowEditorPanel.js';
|
|
6
6
|
import { renderQueryResultsPane } from '../components/queryResults.js';
|
|
7
|
-
import {
|
|
7
|
+
import { getQueryMessages, getQueryPerformance } from '../store.js';
|
|
8
8
|
import { escapeHtml, formatBytes, formatCellValue, formatExecutionTimeMs, formatNumber, isBlobPreview } from '../utils/format.js';
|
|
9
9
|
|
|
10
10
|
function renderMissingDatabase() {
|
|
@@ -244,7 +244,6 @@ function renderResultsSurface(state, isResultsRoute) {
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
export function renderEditorView(state, { isResultsRoute = false } = {}) {
|
|
247
|
-
const connection = getCurrentConnection(state);
|
|
248
247
|
const editorSectionClass = state.editor.editorPanelVisible ? 'min-h-[27.5%] max-h-[60%]' : 'flex-none';
|
|
249
248
|
const resultsSectionClass = 'flex-1';
|
|
250
249
|
|
|
@@ -261,7 +260,6 @@ export function renderEditorView(state, { isResultsRoute = false } = {}) {
|
|
|
261
260
|
historyTotal: state.editor.historyTotal,
|
|
262
261
|
editorVisible: state.editor.editorPanelVisible,
|
|
263
262
|
historyVisible: state.editor.historyPanelVisible,
|
|
264
|
-
title: connection?.label ?? 'SQLite Query Workspace',
|
|
265
263
|
})}
|
|
266
264
|
</section>
|
|
267
265
|
<section class="${resultsSectionClass} flex min-h-0 min-w-0 flex-col overflow-hidden">
|
|
@@ -2,7 +2,7 @@ import { clearInspector, renderDdlSection, renderInspector } from "../components
|
|
|
2
2
|
import { escapeHtml, formatNumber } from "../utils/format.js";
|
|
3
3
|
|
|
4
4
|
function renderEntryGroup(title, entries, activeName, options = {}) {
|
|
5
|
-
const { compact = false, showMeta = true } = options;
|
|
5
|
+
const { compact = false, showMeta = true, emptyMessage = null } = options;
|
|
6
6
|
const entriesMarkup = entries.length
|
|
7
7
|
? [
|
|
8
8
|
'<div class="space-y-2">',
|
|
@@ -51,8 +51,8 @@ function renderEntryGroup(title, entries, activeName, options = {}) {
|
|
|
51
51
|
].join("")
|
|
52
52
|
: [
|
|
53
53
|
'<div class="text-sm text-on-surface-variant/45">No ',
|
|
54
|
-
escapeHtml(title.toLowerCase())
|
|
55
|
-
"
|
|
54
|
+
emptyMessage ? escapeHtml(emptyMessage) : `${escapeHtml(title.toLowerCase())} found.`,
|
|
55
|
+
"</div>",
|
|
56
56
|
].join("");
|
|
57
57
|
|
|
58
58
|
return `
|
|
@@ -65,6 +65,34 @@ function renderEntryGroup(title, entries, activeName, options = {}) {
|
|
|
65
65
|
`;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
function getFilteredTables(tables, searchQuery) {
|
|
69
|
+
const normalizedSearch = String(searchQuery ?? "")
|
|
70
|
+
.trim()
|
|
71
|
+
.toLowerCase();
|
|
72
|
+
|
|
73
|
+
if (!normalizedSearch) {
|
|
74
|
+
return tables;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return tables.filter((table) => table.name.toLowerCase().includes(normalizedSearch));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function renderStructureTableSearch(state) {
|
|
81
|
+
return `
|
|
82
|
+
<label class="table-designer-sidebar__search">
|
|
83
|
+
<span class="material-symbols-outlined text-sm text-on-surface-variant/55">search</span>
|
|
84
|
+
<input
|
|
85
|
+
class="table-designer-sidebar__search-input"
|
|
86
|
+
data-bind="structure-table-search"
|
|
87
|
+
placeholder="Search tables..."
|
|
88
|
+
spellcheck="false"
|
|
89
|
+
type="search"
|
|
90
|
+
value="${escapeHtml(state.structure.tableSearchQuery ?? "")}"
|
|
91
|
+
/>
|
|
92
|
+
</label>
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
68
96
|
function renderLoadingInspector() {
|
|
69
97
|
return `
|
|
70
98
|
<div class="structure-graph__panel is-empty">
|
|
@@ -308,6 +336,9 @@ function renderStructureWorkspaceHeader(structure, selectedName) {
|
|
|
308
336
|
|
|
309
337
|
export function renderStructureView(state) {
|
|
310
338
|
const structure = state.structure.data;
|
|
339
|
+
const filteredTables = structure
|
|
340
|
+
? getFilteredTables(structure.grouped.tables ?? [], state.structure.tableSearchQuery)
|
|
341
|
+
: [];
|
|
311
342
|
const detail =
|
|
312
343
|
state.structure.detail?.name === state.structure.selectedName ? state.structure.detail : null;
|
|
313
344
|
const tablesVisible = state.structure.tablesVisible !== false;
|
|
@@ -334,15 +365,22 @@ export function renderStructureView(state) {
|
|
|
334
365
|
)}
|
|
335
366
|
</div>
|
|
336
367
|
</div>
|
|
368
|
+
${renderStructureTableSearch(state)}
|
|
337
369
|
<div class="structure-view__sidebar-body custom-scrollbar">
|
|
338
370
|
${
|
|
339
371
|
structure
|
|
340
372
|
? `
|
|
341
373
|
${renderEntryGroup(
|
|
342
374
|
"Tables",
|
|
343
|
-
|
|
375
|
+
filteredTables,
|
|
344
376
|
state.structure.selectedName,
|
|
345
|
-
{
|
|
377
|
+
{
|
|
378
|
+
compact: true,
|
|
379
|
+
showMeta: false,
|
|
380
|
+
emptyMessage: state.structure.tableSearchQuery
|
|
381
|
+
? "tables match the current search."
|
|
382
|
+
: null,
|
|
383
|
+
}
|
|
346
384
|
)}
|
|
347
385
|
${renderEntryGroup(
|
|
348
386
|
"Views",
|