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
package/frontend/js/api.js
CHANGED
|
@@ -366,6 +366,15 @@ export function getDataTable(tableName, options = {}) {
|
|
|
366
366
|
params.set("sortDirection", String(options.sortDirection));
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
const filterColumn = String(options.filterColumn ?? "").trim();
|
|
370
|
+
const filterValue = String(options.filterValue ?? "");
|
|
371
|
+
|
|
372
|
+
if (filterColumn && filterValue.trim()) {
|
|
373
|
+
params.set("filterColumn", filterColumn);
|
|
374
|
+
params.set("filterOperator", String(options.filterOperator ?? "="));
|
|
375
|
+
params.set("filterValue", filterValue);
|
|
376
|
+
}
|
|
377
|
+
|
|
369
378
|
const query = params.toString();
|
|
370
379
|
|
|
371
380
|
return request(`/api/data/${encodeURIComponent(tableName)}${query ? `?${query}` : ""}`);
|
|
@@ -385,6 +394,13 @@ export function updateDataTableRow(tableName, payload) {
|
|
|
385
394
|
});
|
|
386
395
|
}
|
|
387
396
|
|
|
397
|
+
export function previewDataTableRowUpdate(tableName, payload) {
|
|
398
|
+
return request(`/api/data/${encodeURIComponent(tableName)}/rows/preview-update`, {
|
|
399
|
+
method: "POST",
|
|
400
|
+
body: payload,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
388
404
|
export function deleteDataTableRow(tableName, payload) {
|
|
389
405
|
return request(`/api/data/${encodeURIComponent(tableName)}/rows`, {
|
|
390
406
|
method: "DELETE",
|
package/frontend/js/app.js
CHANGED
|
@@ -49,6 +49,8 @@ import {
|
|
|
49
49
|
openEditConnectionModal,
|
|
50
50
|
openCreateQueryChartModal,
|
|
51
51
|
openEditQueryChartModal,
|
|
52
|
+
openDataRowUpdatePreview,
|
|
53
|
+
openEditorRowUpdatePreview,
|
|
52
54
|
refreshCurrentRoute,
|
|
53
55
|
refreshMediaTaggingPreview,
|
|
54
56
|
removeConnection,
|
|
@@ -66,9 +68,12 @@ import {
|
|
|
66
68
|
selectStructureEntry,
|
|
67
69
|
setTableDesignerSearchQuery,
|
|
68
70
|
setTableDesignerSqlPreviewVisibility,
|
|
71
|
+
setDataTableSearchQuery,
|
|
72
|
+
setStructureTableSearchQuery,
|
|
69
73
|
toggleStructureTablesPanel,
|
|
70
74
|
setDataPage,
|
|
71
75
|
setDataPageSize,
|
|
76
|
+
setDataFilterOperator,
|
|
72
77
|
setDataSearchColumn,
|
|
73
78
|
setDataSearchQuery,
|
|
74
79
|
toggleDataTablesPanel,
|
|
@@ -82,6 +87,7 @@ import {
|
|
|
82
87
|
submitCreateMediaTaggingTagTable,
|
|
83
88
|
submitCreateMediaTaggingMappingTable,
|
|
84
89
|
submitDeleteQueryHistoryConfirmation,
|
|
90
|
+
submitRowUpdatePreviewConfirmation,
|
|
85
91
|
setQueryHistoryPanelVisibility,
|
|
86
92
|
sortDataTableByColumn,
|
|
87
93
|
sortEditorResultsByColumn,
|
|
@@ -100,8 +106,6 @@ import {
|
|
|
100
106
|
submitCreateConnection,
|
|
101
107
|
createCurrentMediaTag,
|
|
102
108
|
submitDeleteRowConfirmation,
|
|
103
|
-
submitDataRowUpdate,
|
|
104
|
-
submitEditorRowUpdate,
|
|
105
109
|
submitEditConnection,
|
|
106
110
|
submitImportSql,
|
|
107
111
|
submitOpenConnection,
|
|
@@ -1771,7 +1775,17 @@ document.addEventListener('input', event => {
|
|
|
1771
1775
|
}
|
|
1772
1776
|
|
|
1773
1777
|
if (bindNode.dataset.bind === 'data-search-query') {
|
|
1774
|
-
setDataSearchQuery(bindNode.value);
|
|
1778
|
+
void setDataSearchQuery(bindNode.value);
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
if (bindNode.dataset.bind === 'data-table-search') {
|
|
1783
|
+
setDataTableSearchQuery(bindNode.value);
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
if (bindNode.dataset.bind === 'structure-table-search') {
|
|
1788
|
+
setStructureTableSearchQuery(bindNode.value);
|
|
1775
1789
|
return;
|
|
1776
1790
|
}
|
|
1777
1791
|
|
|
@@ -1870,7 +1884,12 @@ document.addEventListener('change', event => {
|
|
|
1870
1884
|
}
|
|
1871
1885
|
|
|
1872
1886
|
if (bindNode.dataset.bind === 'data-search-column') {
|
|
1873
|
-
setDataSearchColumn(bindNode.value);
|
|
1887
|
+
void setDataSearchColumn(bindNode.value);
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
if (bindNode.dataset.bind === 'data-filter-operator') {
|
|
1892
|
+
void setDataFilterOperator(bindNode.value);
|
|
1874
1893
|
return;
|
|
1875
1894
|
}
|
|
1876
1895
|
|
|
@@ -2055,6 +2074,9 @@ document.addEventListener('submit', async event => {
|
|
|
2055
2074
|
case 'delete-query-history-confirm':
|
|
2056
2075
|
await submitDeleteQueryHistoryConfirmation();
|
|
2057
2076
|
return;
|
|
2077
|
+
case 'apply-row-update-preview':
|
|
2078
|
+
await submitRowUpdatePreviewConfirmation();
|
|
2079
|
+
return;
|
|
2058
2080
|
case 'create-media-tagging-tag-table':
|
|
2059
2081
|
await submitCreateMediaTaggingTagTable();
|
|
2060
2082
|
return;
|
|
@@ -2083,7 +2105,7 @@ document.addEventListener('submit', async event => {
|
|
|
2083
2105
|
}
|
|
2084
2106
|
}
|
|
2085
2107
|
|
|
2086
|
-
await
|
|
2108
|
+
await openDataRowUpdatePreview(String(formData.get('rowIndex') ?? ''), values, rowIdentity);
|
|
2087
2109
|
return;
|
|
2088
2110
|
}
|
|
2089
2111
|
case 'save-editor-row': {
|
|
@@ -2097,7 +2119,7 @@ document.addEventListener('submit', async event => {
|
|
|
2097
2119
|
values[key.slice('field:'.length)] = String(value ?? '');
|
|
2098
2120
|
}
|
|
2099
2121
|
|
|
2100
|
-
await
|
|
2122
|
+
await openEditorRowUpdatePreview(String(formData.get('rowIndex') ?? ''), values);
|
|
2101
2123
|
return;
|
|
2102
2124
|
}
|
|
2103
2125
|
case 'save-query-history-title': {
|
|
@@ -425,6 +425,98 @@ function renderDeleteRowConfirmForm(modal) {
|
|
|
425
425
|
].join("");
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
+
function renderRowUpdatePreviewForm(modal) {
|
|
429
|
+
const preview = modal.preview ?? {};
|
|
430
|
+
const changes = preview.changes ?? [];
|
|
431
|
+
const params = preview.params ?? [];
|
|
432
|
+
const warnings = preview.warnings ?? [];
|
|
433
|
+
const changesMarkup = changes.length
|
|
434
|
+
? [
|
|
435
|
+
'<div class="overflow-hidden border border-outline-variant/10">',
|
|
436
|
+
'<table class="w-full text-left text-sm">',
|
|
437
|
+
'<thead class="bg-surface-container-highest text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">',
|
|
438
|
+
'<tr><th class="px-3 py-2">Column</th><th class="px-3 py-2">Old</th><th class="px-3 py-2">New</th></tr>',
|
|
439
|
+
'</thead><tbody class="divide-y divide-outline-variant/10">',
|
|
440
|
+
changes
|
|
441
|
+
.map(
|
|
442
|
+
change => `
|
|
443
|
+
<tr>
|
|
444
|
+
<td class="px-3 py-2 font-mono text-xs text-primary-container">${escapeHtml(change.column)}</td>
|
|
445
|
+
<td class="px-3 py-2 text-on-surface-variant/70">${escapeHtml(change.oldValue)}</td>
|
|
446
|
+
<td class="px-3 py-2 text-on-surface">${escapeHtml(change.newValue)}</td>
|
|
447
|
+
</tr>
|
|
448
|
+
`,
|
|
449
|
+
)
|
|
450
|
+
.join(''),
|
|
451
|
+
'</tbody></table></div>',
|
|
452
|
+
].join('')
|
|
453
|
+
: '<div class="border border-outline-variant/10 bg-surface-container-low px-4 py-3 text-sm text-on-surface-variant/65">No changed values were detected.</div>';
|
|
454
|
+
const paramsMarkup = params.length
|
|
455
|
+
? `
|
|
456
|
+
<div class="space-y-2">
|
|
457
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">Parameters</div>
|
|
458
|
+
<div class="space-y-2">
|
|
459
|
+
${params
|
|
460
|
+
.map(
|
|
461
|
+
param => `
|
|
462
|
+
<div class="flex gap-3 border border-outline-variant/10 bg-surface-container-low px-3 py-2 text-xs">
|
|
463
|
+
<span class="font-mono text-primary-container">$${escapeHtml(param.index)}</span>
|
|
464
|
+
<span class="min-w-0 break-words text-on-surface">${escapeHtml(param.value)}</span>
|
|
465
|
+
</div>
|
|
466
|
+
`,
|
|
467
|
+
)
|
|
468
|
+
.join('')}
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
`
|
|
472
|
+
: '';
|
|
473
|
+
const warningsMarkup = warnings.length
|
|
474
|
+
? `
|
|
475
|
+
<div class="space-y-2">
|
|
476
|
+
${warnings
|
|
477
|
+
.map(
|
|
478
|
+
warning => `
|
|
479
|
+
<div class="border border-primary-container/20 bg-primary-container/10 px-4 py-3 text-sm text-on-surface">
|
|
480
|
+
${escapeHtml(warning)}
|
|
481
|
+
</div>
|
|
482
|
+
`,
|
|
483
|
+
)
|
|
484
|
+
.join('')}
|
|
485
|
+
</div>
|
|
486
|
+
`
|
|
487
|
+
: '';
|
|
488
|
+
|
|
489
|
+
return `
|
|
490
|
+
<form class="space-y-5" data-form="apply-row-update-preview">
|
|
491
|
+
<div class="space-y-2">
|
|
492
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
493
|
+
${escapeHtml(modal.tableName ?? 'Table Row')}
|
|
494
|
+
</div>
|
|
495
|
+
<p class="text-sm leading-7 text-on-surface-variant/70">
|
|
496
|
+
Review the SQL and changed values before applying this row update.
|
|
497
|
+
</p>
|
|
498
|
+
</div>
|
|
499
|
+
${warningsMarkup}
|
|
500
|
+
<div class="space-y-2">
|
|
501
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">SQL Preview</div>
|
|
502
|
+
${renderSqlPreviewField(preview.sql ?? '', 'sql-highlight-shell--compact')}
|
|
503
|
+
</div>
|
|
504
|
+
<div class="space-y-2">
|
|
505
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">Changed Values</div>
|
|
506
|
+
${changesMarkup}
|
|
507
|
+
</div>
|
|
508
|
+
${paramsMarkup}
|
|
509
|
+
${renderError(modal.error)}
|
|
510
|
+
<div class="flex items-center justify-end gap-3 pt-2">
|
|
511
|
+
<button class="standard-button" data-action="close-modal" type="button">Cancel</button>
|
|
512
|
+
<button class="signature-button" type="submit">
|
|
513
|
+
${modal.submitting ? 'Applying...' : 'Apply Changes'}
|
|
514
|
+
</button>
|
|
515
|
+
</div>
|
|
516
|
+
</form>
|
|
517
|
+
`;
|
|
518
|
+
}
|
|
519
|
+
|
|
428
520
|
function renderChartColumnOptions(analysis, { allowEmpty = false, includeNumericHint = false } = {}) {
|
|
429
521
|
const options = allowEmpty ? [{ value: "", label: "None" }] : [];
|
|
430
522
|
|
|
@@ -869,6 +961,11 @@ export function renderModal(state) {
|
|
|
869
961
|
title: "Delete Row",
|
|
870
962
|
body: renderDeleteRowConfirmForm(modal),
|
|
871
963
|
},
|
|
964
|
+
"row-update-preview": {
|
|
965
|
+
eyebrow: "Mutation // Review row update",
|
|
966
|
+
title: "Review Update",
|
|
967
|
+
body: renderRowUpdatePreviewForm(modal),
|
|
968
|
+
},
|
|
872
969
|
"chart-editor": {
|
|
873
970
|
eyebrow: "Charts // Configure query-based ECharts panel",
|
|
874
971
|
title: modal.draft?.mode === "edit" ? "Edit Chart" : "New Chart",
|
|
@@ -904,7 +1001,7 @@ export function renderModal(state) {
|
|
|
904
1001
|
|
|
905
1002
|
return `
|
|
906
1003
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-background/85 px-4 backdrop-blur-sm">
|
|
907
|
-
<div class="w-full ${modal.kind === "chart-editor" ? "max-w-3xl" : "max-w-xl"} border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
|
|
1004
|
+
<div class="w-full ${modal.kind === "chart-editor" || modal.kind === "row-update-preview" ? "max-w-3xl" : "max-w-xl"} border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
|
|
908
1005
|
<div class="flex items-start justify-between gap-4 border-b border-outline-variant/10 bg-surface-container-low px-6 py-5">
|
|
909
1006
|
<div>
|
|
910
1007
|
<div class="text-[10px] font-mono uppercase tracking-[0.26em] text-primary-container/70">
|
|
@@ -38,7 +38,6 @@ function renderEditorSurface({ query }) {
|
|
|
38
38
|
|
|
39
39
|
export function renderQueryEditor({
|
|
40
40
|
query,
|
|
41
|
-
title,
|
|
42
41
|
executing = false,
|
|
43
42
|
exporting = false,
|
|
44
43
|
historyLoading = false,
|
|
@@ -48,13 +47,7 @@ export function renderQueryEditor({
|
|
|
48
47
|
}) {
|
|
49
48
|
const secondaryButtonClass = "standard-button";
|
|
50
49
|
const left = `
|
|
51
|
-
<div class="flex items-center gap-2
|
|
52
|
-
<span class="material-symbols-outlined text-xs text-[#FCE300]">database</span>
|
|
53
|
-
<span class="text-[10px] font-mono uppercase tracking-widest text-on-surface-variant">${escapeHtml(
|
|
54
|
-
title
|
|
55
|
-
)}</span>
|
|
56
|
-
</div>
|
|
57
|
-
<div class="hidden items-center gap-2 text-[10px] font-mono uppercase tracking-widest text-on-surface-variant/40 md:flex">
|
|
50
|
+
<div class="flex items-center gap-2 text-[10px] font-mono uppercase tracking-widest text-on-surface-variant/40">
|
|
58
51
|
<span class="material-symbols-outlined text-xs">history</span>
|
|
59
52
|
${historyLoading ? "Loading history..." : `${historyTotal} queries tracked`}
|
|
60
53
|
</div>
|