sqlite-hub 0.3.1 → 0.3.2
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/README.md +7 -1
- package/changelog.md +5 -0
- package/js/api.js +7 -0
- package/js/app.js +12 -0
- package/js/components/modal.js +74 -0
- package/js/components/rowEditorPanel.js +63 -33
- package/js/store.js +204 -1
- package/js/views/data.js +4 -0
- package/js/views/editor.js +4 -0
- package/package.json +2 -2
- package/server/routes/data.js +14 -0
- package/server/server.js +22 -1
- package/server/services/sqlite/dataBrowserService.js +30 -0
package/README.md
CHANGED
package/changelog.md
CHANGED
package/js/api.js
CHANGED
|
@@ -179,6 +179,13 @@ export function updateDataTableRow(tableName, payload) {
|
|
|
179
179
|
});
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
export function deleteDataTableRow(tableName, payload) {
|
|
183
|
+
return request(`/api/data/${encodeURIComponent(tableName)}/rows`, {
|
|
184
|
+
method: "DELETE",
|
|
185
|
+
body: payload,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
182
189
|
export function getSettings() {
|
|
183
190
|
return request("/api/settings");
|
|
184
191
|
}
|
package/js/app.js
CHANGED
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
initializeApp,
|
|
27
27
|
loadQueryFromHistory,
|
|
28
28
|
openModal,
|
|
29
|
+
openDeleteDataRowModal,
|
|
30
|
+
openDeleteEditorRowModal,
|
|
29
31
|
openEditConnectionModal,
|
|
30
32
|
refreshCurrentRoute,
|
|
31
33
|
removeConnection,
|
|
@@ -39,6 +41,7 @@ import {
|
|
|
39
41
|
setEditorTab,
|
|
40
42
|
setRoute,
|
|
41
43
|
submitCreateConnection,
|
|
44
|
+
submitDeleteRowConfirmation,
|
|
42
45
|
submitDataRowUpdate,
|
|
43
46
|
submitEditorRowUpdate,
|
|
44
47
|
submitEditConnection,
|
|
@@ -302,6 +305,12 @@ async function handleAction(actionNode) {
|
|
|
302
305
|
router.navigate(success ? "/editor/results" : "/editor");
|
|
303
306
|
return;
|
|
304
307
|
}
|
|
308
|
+
case "delete-data-row":
|
|
309
|
+
openDeleteDataRowModal(actionNode.dataset.rowIndex);
|
|
310
|
+
return;
|
|
311
|
+
case "delete-editor-row":
|
|
312
|
+
openDeleteEditorRowModal(actionNode.dataset.rowIndex);
|
|
313
|
+
return;
|
|
305
314
|
case "clear-query":
|
|
306
315
|
clearCurrentQuery();
|
|
307
316
|
return;
|
|
@@ -516,6 +525,9 @@ document.addEventListener("submit", async (event) => {
|
|
|
516
525
|
|
|
517
526
|
return;
|
|
518
527
|
}
|
|
528
|
+
case "delete-row-confirm":
|
|
529
|
+
await submitDeleteRowConfirmation();
|
|
530
|
+
return;
|
|
519
531
|
case "save-data-row": {
|
|
520
532
|
const values = {};
|
|
521
533
|
|
package/js/components/modal.js
CHANGED
|
@@ -254,6 +254,75 @@ function renderImportSqlForm(modal, state) {
|
|
|
254
254
|
`;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
function renderDeleteRowConfirmForm(modal) {
|
|
258
|
+
const rowPreview = modal.rowPreview ?? [];
|
|
259
|
+
|
|
260
|
+
return `
|
|
261
|
+
<form class="space-y-5" data-form="delete-row-confirm">
|
|
262
|
+
<div class="space-y-3">
|
|
263
|
+
<p class="text-sm leading-7 text-on-surface">
|
|
264
|
+
Delete this row from <span class="font-bold text-primary-container">${escapeHtml(
|
|
265
|
+
modal.tableName ?? "the current table"
|
|
266
|
+
)}</span>?
|
|
267
|
+
</p>
|
|
268
|
+
<p class="text-sm leading-7 text-on-surface-variant/65">
|
|
269
|
+
This action cannot be undone.
|
|
270
|
+
</p>
|
|
271
|
+
${
|
|
272
|
+
modal.rowLabel
|
|
273
|
+
? `
|
|
274
|
+
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
275
|
+
${escapeHtml(modal.rowLabel)}
|
|
276
|
+
</div>
|
|
277
|
+
`
|
|
278
|
+
: ""
|
|
279
|
+
}
|
|
280
|
+
${
|
|
281
|
+
rowPreview.length
|
|
282
|
+
? `
|
|
283
|
+
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
|
284
|
+
${rowPreview
|
|
285
|
+
.map(
|
|
286
|
+
(field) => `
|
|
287
|
+
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
|
|
288
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
289
|
+
${escapeHtml(field.label)}
|
|
290
|
+
</div>
|
|
291
|
+
<div
|
|
292
|
+
class="mt-2 text-sm text-on-surface"
|
|
293
|
+
title="${escapeHtml(field.fullValue ?? field.value ?? "")}"
|
|
294
|
+
>
|
|
295
|
+
${escapeHtml(field.value ?? "")}
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
`
|
|
299
|
+
)
|
|
300
|
+
.join("")}
|
|
301
|
+
</div>
|
|
302
|
+
`
|
|
303
|
+
: ""
|
|
304
|
+
}
|
|
305
|
+
</div>
|
|
306
|
+
${renderError(modal.error)}
|
|
307
|
+
<div class="flex items-center justify-end gap-3 pt-2">
|
|
308
|
+
<button
|
|
309
|
+
class="border border-outline-variant/20 px-4 py-3 text-xs font-bold uppercase tracking-[0.18em] text-on-surface-variant hover:bg-surface-container-highest"
|
|
310
|
+
data-action="close-modal"
|
|
311
|
+
type="button"
|
|
312
|
+
>
|
|
313
|
+
Cancel
|
|
314
|
+
</button>
|
|
315
|
+
<button
|
|
316
|
+
class="border border-error/25 bg-error-container/10 px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-error"
|
|
317
|
+
type="submit"
|
|
318
|
+
>
|
|
319
|
+
${modal.submitting ? "Deleting..." : "Delete Row"}
|
|
320
|
+
</button>
|
|
321
|
+
</div>
|
|
322
|
+
</form>
|
|
323
|
+
`;
|
|
324
|
+
}
|
|
325
|
+
|
|
257
326
|
export function renderModal(state) {
|
|
258
327
|
const modal = state.modal;
|
|
259
328
|
|
|
@@ -282,6 +351,11 @@ export function renderModal(state) {
|
|
|
282
351
|
title: "Edit Connection",
|
|
283
352
|
body: renderEditConnectionForm(modal),
|
|
284
353
|
},
|
|
354
|
+
"delete-row": {
|
|
355
|
+
eyebrow: "Mutation // Confirm row deletion",
|
|
356
|
+
title: "Delete Row",
|
|
357
|
+
body: renderDeleteRowConfirmForm(modal),
|
|
358
|
+
},
|
|
285
359
|
};
|
|
286
360
|
|
|
287
361
|
const config = contentByKind[modal.kind];
|
|
@@ -23,16 +23,23 @@ export function renderRowEditorPanel({
|
|
|
23
23
|
disabledMessage = "",
|
|
24
24
|
saveError = null,
|
|
25
25
|
saving = false,
|
|
26
|
+
deleting = false,
|
|
26
27
|
reloadAction = "",
|
|
27
|
-
submitLabel = "Save
|
|
28
|
+
submitLabel = "Save",
|
|
29
|
+
deleteAction = "",
|
|
30
|
+
deleteRowIndex = null,
|
|
31
|
+
deleteLabel = "Delete Row",
|
|
32
|
+
deleteEnabled = false,
|
|
28
33
|
emptyEditableMessage = "This row has no editable scalar columns.",
|
|
29
34
|
}) {
|
|
30
35
|
const canSubmit = !disabledMessage && editableFields.length > 0;
|
|
36
|
+
const canDelete = !disabledMessage && deleteEnabled;
|
|
37
|
+
const formId = `${formName}-panel-form`;
|
|
31
38
|
|
|
32
39
|
return `
|
|
33
40
|
<section class="flex h-full min-h-0 flex-col bg-surface-low">
|
|
34
41
|
<header class="border-b border-outline-variant/10 bg-surface-container px-6 py-5">
|
|
35
|
-
<div class="
|
|
42
|
+
<div class="space-y-4">
|
|
36
43
|
<div>
|
|
37
44
|
<div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
|
|
38
45
|
${escapeHtml(sectionLabel)}
|
|
@@ -50,13 +57,58 @@ export function renderRowEditorPanel({
|
|
|
50
57
|
: ""
|
|
51
58
|
}
|
|
52
59
|
</div>
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
<div class="flex flex-wrap items-center justify-end gap-2">
|
|
61
|
+
${
|
|
62
|
+
reloadAction
|
|
63
|
+
? `
|
|
64
|
+
<button
|
|
65
|
+
class="border border-outline-variant/20 px-4 py-3 text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface hover:bg-surface-container-highest"
|
|
66
|
+
data-action="${escapeHtml(reloadAction)}"
|
|
67
|
+
type="button"
|
|
68
|
+
>
|
|
69
|
+
Reload
|
|
70
|
+
</button>
|
|
71
|
+
`
|
|
72
|
+
: ""
|
|
73
|
+
}
|
|
74
|
+
${
|
|
75
|
+
canSubmit
|
|
76
|
+
? `
|
|
77
|
+
<button
|
|
78
|
+
class="bg-primary-container px-5 py-3 text-[10px] font-black uppercase tracking-[0.16em] text-on-primary disabled:cursor-default disabled:opacity-40"
|
|
79
|
+
form="${escapeHtml(formId)}"
|
|
80
|
+
type="submit"
|
|
81
|
+
${saving || deleting ? "disabled" : ""}
|
|
82
|
+
>
|
|
83
|
+
${escapeHtml(saving ? "Saving..." : submitLabel)}
|
|
84
|
+
</button>
|
|
85
|
+
`
|
|
86
|
+
: ""
|
|
87
|
+
}
|
|
88
|
+
${
|
|
89
|
+
canDelete
|
|
90
|
+
? `
|
|
91
|
+
<button
|
|
92
|
+
class="border border-error/25 bg-error-container/10 px-4 py-3 text-[10px] font-bold uppercase tracking-[0.16em] text-error transition-colors hover:bg-error-container/20 disabled:cursor-default disabled:opacity-40"
|
|
93
|
+
data-action="${escapeHtml(deleteAction)}"
|
|
94
|
+
data-row-index="${escapeHtml(String(deleteRowIndex ?? ""))}"
|
|
95
|
+
type="button"
|
|
96
|
+
${saving || deleting ? "disabled" : ""}
|
|
97
|
+
>
|
|
98
|
+
${escapeHtml(deleting ? "Deleting..." : deleteLabel)}
|
|
99
|
+
</button>
|
|
100
|
+
`
|
|
101
|
+
: ""
|
|
102
|
+
}
|
|
103
|
+
<button
|
|
104
|
+
aria-label="Close panel"
|
|
105
|
+
class="flex h-11 w-11 items-center justify-center border border-outline-variant/20 text-on-surface hover:bg-surface-container-highest"
|
|
106
|
+
data-action="${escapeHtml(closeAction)}"
|
|
107
|
+
type="button"
|
|
108
|
+
>
|
|
109
|
+
<span class="material-symbols-outlined text-base">close</span>
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
60
112
|
</div>
|
|
61
113
|
</header>
|
|
62
114
|
<div class="custom-scrollbar flex-1 overflow-auto px-6 py-6">
|
|
@@ -68,7 +120,7 @@ export function renderRowEditorPanel({
|
|
|
68
120
|
</div>
|
|
69
121
|
`
|
|
70
122
|
: `
|
|
71
|
-
<form class="space-y-6" data-form="${escapeHtml(formName)}">
|
|
123
|
+
<form class="space-y-6" data-form="${escapeHtml(formName)}" id="${escapeHtml(formId)}">
|
|
72
124
|
${hiddenFields
|
|
73
125
|
.map(
|
|
74
126
|
(field) => `
|
|
@@ -92,7 +144,7 @@ export function renderRowEditorPanel({
|
|
|
92
144
|
${escapeHtml(field.label ?? field.name)}
|
|
93
145
|
</span>
|
|
94
146
|
<textarea
|
|
95
|
-
class="min-h-[
|
|
147
|
+
class="min-h-[56px] w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
96
148
|
name="field:${escapeHtml(field.name)}"
|
|
97
149
|
>${escapeHtml(field.value ?? "")}</textarea>
|
|
98
150
|
</label>
|
|
@@ -117,28 +169,6 @@ export function renderRowEditorPanel({
|
|
|
117
169
|
`
|
|
118
170
|
: ""
|
|
119
171
|
}
|
|
120
|
-
<div class="flex items-center justify-end gap-3 border-t border-outline-variant/10 pt-6">
|
|
121
|
-
${
|
|
122
|
-
reloadAction
|
|
123
|
-
? `
|
|
124
|
-
<button
|
|
125
|
-
class="border border-outline-variant/20 px-4 py-3 text-xs font-bold uppercase tracking-[0.18em] text-on-surface hover:bg-surface-container-highest"
|
|
126
|
-
data-action="${escapeHtml(reloadAction)}"
|
|
127
|
-
type="button"
|
|
128
|
-
>
|
|
129
|
-
Reload
|
|
130
|
-
</button>
|
|
131
|
-
`
|
|
132
|
-
: ""
|
|
133
|
-
}
|
|
134
|
-
<button
|
|
135
|
-
class="bg-primary-container px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-on-primary disabled:cursor-default disabled:opacity-40"
|
|
136
|
-
type="submit"
|
|
137
|
-
${canSubmit ? "" : "disabled"}
|
|
138
|
-
>
|
|
139
|
-
${escapeHtml(saving ? "Saving..." : submitLabel)}
|
|
140
|
-
</button>
|
|
141
|
-
</div>
|
|
142
172
|
</form>
|
|
143
173
|
`
|
|
144
174
|
}
|
package/js/store.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as api from "./api.js";
|
|
2
|
-
import { inferStatusTone } from "./utils/format.js";
|
|
2
|
+
import { formatCellValue, inferStatusTone, truncateMiddle } from "./utils/format.js";
|
|
3
3
|
|
|
4
4
|
const listeners = new Set();
|
|
5
5
|
const DEFAULT_SETTINGS = {
|
|
@@ -45,6 +45,7 @@ const state = {
|
|
|
45
45
|
loading: false,
|
|
46
46
|
tableLoading: false,
|
|
47
47
|
saving: false,
|
|
48
|
+
deleting: false,
|
|
48
49
|
page: 1,
|
|
49
50
|
pageSize: 50,
|
|
50
51
|
selectedRowIndex: null,
|
|
@@ -64,6 +65,7 @@ const state = {
|
|
|
64
65
|
exportLoading: false,
|
|
65
66
|
selectedRowIndex: null,
|
|
66
67
|
saving: false,
|
|
68
|
+
deleting: false,
|
|
67
69
|
saveError: null,
|
|
68
70
|
},
|
|
69
71
|
structure: {
|
|
@@ -140,6 +142,32 @@ function getCurrentStructureEntry(snapshot = state) {
|
|
|
140
142
|
return entries.find((entry) => entry.name === snapshot.structure.selectedName) ?? null;
|
|
141
143
|
}
|
|
142
144
|
|
|
145
|
+
function buildDeleteRowPreview(fields = []) {
|
|
146
|
+
return fields
|
|
147
|
+
.filter((field) => field && field.label)
|
|
148
|
+
.slice(0, 8)
|
|
149
|
+
.map((field) => {
|
|
150
|
+
const fullValue = formatCellValue(field.value);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
label: String(field.label),
|
|
154
|
+
value: truncateMiddle(fullValue, 96),
|
|
155
|
+
fullValue,
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function buildFallbackDeleteRowPreview(row) {
|
|
161
|
+
return buildDeleteRowPreview(
|
|
162
|
+
Object.entries(row ?? {})
|
|
163
|
+
.filter(([key]) => key !== "__identity")
|
|
164
|
+
.map(([key, value]) => ({
|
|
165
|
+
label: key,
|
|
166
|
+
value,
|
|
167
|
+
}))
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
143
171
|
function clearRouteSlices() {
|
|
144
172
|
state.overview.error = null;
|
|
145
173
|
state.dataBrowser.error = null;
|
|
@@ -621,6 +649,69 @@ export function openEditConnectionModal(id) {
|
|
|
621
649
|
emitChange();
|
|
622
650
|
}
|
|
623
651
|
|
|
652
|
+
export function openDeleteDataRowModal(rowIndex) {
|
|
653
|
+
const numericIndex = Number(rowIndex);
|
|
654
|
+
const tableName = state.dataBrowser.selectedTable;
|
|
655
|
+
const row = state.dataBrowser.table?.rows?.[numericIndex];
|
|
656
|
+
const rowPreview = buildDeleteRowPreview(
|
|
657
|
+
(state.dataBrowser.table?.columnMeta ?? [])
|
|
658
|
+
.filter((column) => column.visible)
|
|
659
|
+
.map((column) => ({
|
|
660
|
+
label: column.name,
|
|
661
|
+
value: row[column.name],
|
|
662
|
+
}))
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
if (!tableName || !row?.__identity) {
|
|
666
|
+
pushToast("The selected row could not be loaded.", "alert");
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
state.modal = {
|
|
671
|
+
kind: "delete-row",
|
|
672
|
+
target: "data",
|
|
673
|
+
rowIndex: numericIndex,
|
|
674
|
+
tableName,
|
|
675
|
+
rowLabel: `row ${numericIndex + 1}`,
|
|
676
|
+
rowPreview: rowPreview.length ? rowPreview : buildFallbackDeleteRowPreview(row),
|
|
677
|
+
error: null,
|
|
678
|
+
submitting: false,
|
|
679
|
+
};
|
|
680
|
+
emitChange();
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export function openDeleteEditorRowModal(rowIndex) {
|
|
684
|
+
const numericIndex = Number(rowIndex);
|
|
685
|
+
const tableName = state.editor.result?.editing?.tableName ?? null;
|
|
686
|
+
const row = state.editor.result?.rows?.[numericIndex];
|
|
687
|
+
const columns = state.editor.result?.editing?.columns ?? [];
|
|
688
|
+
const rowPreview = buildDeleteRowPreview(
|
|
689
|
+
columns
|
|
690
|
+
.filter((column) => column.visible !== false)
|
|
691
|
+
.map((column) => ({
|
|
692
|
+
label: column.sourceColumn || column.resultName,
|
|
693
|
+
value: row[column.resultName],
|
|
694
|
+
}))
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
if (!tableName || !row?.__identity || !canEditQueryResult()) {
|
|
698
|
+
pushToast("The selected query result row could not be loaded.", "alert");
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
state.modal = {
|
|
703
|
+
kind: "delete-row",
|
|
704
|
+
target: "editor",
|
|
705
|
+
rowIndex: numericIndex,
|
|
706
|
+
tableName,
|
|
707
|
+
rowLabel: `query row ${numericIndex + 1}`,
|
|
708
|
+
rowPreview: rowPreview.length ? rowPreview : buildFallbackDeleteRowPreview(row),
|
|
709
|
+
error: null,
|
|
710
|
+
submitting: false,
|
|
711
|
+
};
|
|
712
|
+
emitChange();
|
|
713
|
+
}
|
|
714
|
+
|
|
624
715
|
export function closeModal() {
|
|
625
716
|
closeModalInternal();
|
|
626
717
|
}
|
|
@@ -992,6 +1083,51 @@ export async function submitDataRowUpdate(rowIndex, values) {
|
|
|
992
1083
|
}
|
|
993
1084
|
}
|
|
994
1085
|
|
|
1086
|
+
export async function submitDataRowDelete(rowIndex, options = {}) {
|
|
1087
|
+
const numericIndex = Number(rowIndex);
|
|
1088
|
+
const tableName = state.dataBrowser.selectedTable;
|
|
1089
|
+
const row = state.dataBrowser.table?.rows?.[numericIndex];
|
|
1090
|
+
const reportErrorToModal = Boolean(options.reportErrorToModal);
|
|
1091
|
+
|
|
1092
|
+
if (!tableName || !row?.__identity) {
|
|
1093
|
+
pushToast("The selected row could not be loaded.", "alert");
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const shouldStepBackPage =
|
|
1098
|
+
(state.dataBrowser.table?.rows?.length ?? 0) <= 1 && state.dataBrowser.page > 1;
|
|
1099
|
+
|
|
1100
|
+
state.dataBrowser.deleting = true;
|
|
1101
|
+
state.dataBrowser.saveError = null;
|
|
1102
|
+
emitChange();
|
|
1103
|
+
|
|
1104
|
+
try {
|
|
1105
|
+
const response = await api.deleteDataTableRow(tableName, {
|
|
1106
|
+
identity: row.__identity,
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
if (shouldStepBackPage) {
|
|
1110
|
+
state.dataBrowser.page -= 1;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
pushToast(response.message || "Table row deleted.", "success");
|
|
1114
|
+
await loadDataTable(++routeLoadVersion);
|
|
1115
|
+
state.dataBrowser.selectedRowIndex = null;
|
|
1116
|
+
return response.data;
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
if (reportErrorToModal) {
|
|
1119
|
+
withModalError(error);
|
|
1120
|
+
} else {
|
|
1121
|
+
state.dataBrowser.saveError = normalizeError(error);
|
|
1122
|
+
emitChange();
|
|
1123
|
+
}
|
|
1124
|
+
return null;
|
|
1125
|
+
} finally {
|
|
1126
|
+
state.dataBrowser.deleting = false;
|
|
1127
|
+
emitChange();
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
995
1131
|
export async function submitEditorRowUpdate(rowIndex, values) {
|
|
996
1132
|
const numericIndex = Number(rowIndex);
|
|
997
1133
|
const result = state.editor.result;
|
|
@@ -1039,6 +1175,73 @@ export async function submitEditorRowUpdate(rowIndex, values) {
|
|
|
1039
1175
|
}
|
|
1040
1176
|
}
|
|
1041
1177
|
|
|
1178
|
+
export async function submitEditorRowDelete(rowIndex, options = {}) {
|
|
1179
|
+
const numericIndex = Number(rowIndex);
|
|
1180
|
+
const result = state.editor.result;
|
|
1181
|
+
const row = result?.rows?.[numericIndex];
|
|
1182
|
+
const tableName = result?.editing?.tableName ?? null;
|
|
1183
|
+
const reportErrorToModal = Boolean(options.reportErrorToModal);
|
|
1184
|
+
|
|
1185
|
+
if (!tableName || !row?.__identity || !canEditQueryResult()) {
|
|
1186
|
+
pushToast("The selected query result row could not be loaded.", "alert");
|
|
1187
|
+
return null;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
state.editor.deleting = true;
|
|
1191
|
+
state.editor.saveError = null;
|
|
1192
|
+
emitChange();
|
|
1193
|
+
|
|
1194
|
+
try {
|
|
1195
|
+
const response = await api.deleteDataTableRow(tableName, {
|
|
1196
|
+
identity: row.__identity,
|
|
1197
|
+
});
|
|
1198
|
+
const nextRows = [...(result.rows ?? [])];
|
|
1199
|
+
|
|
1200
|
+
nextRows.splice(numericIndex, 1);
|
|
1201
|
+
state.editor.result = {
|
|
1202
|
+
...result,
|
|
1203
|
+
rows: nextRows,
|
|
1204
|
+
};
|
|
1205
|
+
state.editor.selectedRowIndex = null;
|
|
1206
|
+
invalidateDatabaseCaches();
|
|
1207
|
+
pushToast(response.message || "Query result row deleted.", "success");
|
|
1208
|
+
emitChange();
|
|
1209
|
+
return response.data;
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
if (reportErrorToModal) {
|
|
1212
|
+
withModalError(error);
|
|
1213
|
+
} else {
|
|
1214
|
+
state.editor.saveError = normalizeError(error);
|
|
1215
|
+
emitChange();
|
|
1216
|
+
}
|
|
1217
|
+
return null;
|
|
1218
|
+
} finally {
|
|
1219
|
+
state.editor.deleting = false;
|
|
1220
|
+
emitChange();
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
export async function submitDeleteRowConfirmation() {
|
|
1225
|
+
const modal = state.modal;
|
|
1226
|
+
|
|
1227
|
+
if (modal?.kind !== "delete-row") {
|
|
1228
|
+
return null;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
startModalSubmission();
|
|
1232
|
+
|
|
1233
|
+
const result =
|
|
1234
|
+
modal.target === "editor"
|
|
1235
|
+
? await submitEditorRowDelete(modal.rowIndex, { reportErrorToModal: true })
|
|
1236
|
+
: await submitDataRowDelete(modal.rowIndex, { reportErrorToModal: true });
|
|
1237
|
+
|
|
1238
|
+
if (result) {
|
|
1239
|
+
closeModalInternal();
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
return result;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1042
1245
|
export async function exportCurrentQueryCsv() {
|
|
1043
1246
|
state.editor.exportLoading = true;
|
|
1044
1247
|
emitChange();
|
package/js/views/data.js
CHANGED
|
@@ -380,6 +380,10 @@ function renderDataRowEditorPanel(state) {
|
|
|
380
380
|
})),
|
|
381
381
|
saveError: state.dataBrowser.saveError,
|
|
382
382
|
saving: state.dataBrowser.saving,
|
|
383
|
+
deleting: state.dataBrowser.deleting,
|
|
384
|
+
deleteAction: "delete-data-row",
|
|
385
|
+
deleteRowIndex: rowIndex,
|
|
386
|
+
deleteEnabled: Boolean(row.__identity),
|
|
383
387
|
reloadAction: "reload-data-route",
|
|
384
388
|
});
|
|
385
389
|
}
|
package/js/views/editor.js
CHANGED
|
@@ -192,6 +192,10 @@ function renderEditorRowPanel(state) {
|
|
|
192
192
|
})),
|
|
193
193
|
saveError: state.editor.saveError,
|
|
194
194
|
saving: state.editor.saving,
|
|
195
|
+
deleting: state.editor.deleting,
|
|
196
|
+
deleteAction: "delete-editor-row",
|
|
197
|
+
deleteRowIndex: rowIndex,
|
|
198
|
+
deleteEnabled: editingState.enabled && Boolean(row.__identity),
|
|
195
199
|
});
|
|
196
200
|
}
|
|
197
201
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sqlite-hub",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "SQLite-only local management app backend and SPA shell",
|
|
5
5
|
"main": "server/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/sqlite-hub.js",
|
|
11
|
-
"dev": "node --watch server/server.js",
|
|
11
|
+
"dev": "node --watch server/server.js --port:4180",
|
|
12
12
|
"publish": "bash publish_brew.sh && bash publish_npm.sh"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
package/server/routes/data.js
CHANGED
|
@@ -51,6 +51,20 @@ function createDataRouter({ dataBrowserService }) {
|
|
|
51
51
|
})
|
|
52
52
|
);
|
|
53
53
|
|
|
54
|
+
router.delete(
|
|
55
|
+
"/:tableName/rows",
|
|
56
|
+
route((req, res) => {
|
|
57
|
+
const data = dataBrowserService.deleteTableRow(req.params.tableName, req.body ?? {});
|
|
58
|
+
|
|
59
|
+
res.json(
|
|
60
|
+
successResponse({
|
|
61
|
+
message: "Table row deleted.",
|
|
62
|
+
data,
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
|
|
54
68
|
return router;
|
|
55
69
|
}
|
|
56
70
|
|
package/server/server.js
CHANGED
|
@@ -107,7 +107,27 @@ app.use("/styles", express.static(path.resolve(__dirname, "..", "styles")));
|
|
|
107
107
|
app.use("/assets", express.static(path.resolve(__dirname, "..", "assets")));
|
|
108
108
|
app.use(errorMiddleware);
|
|
109
109
|
|
|
110
|
-
function
|
|
110
|
+
function parsePortArgument(argv = process.argv.slice(2)) {
|
|
111
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
112
|
+
const argument = argv[index];
|
|
113
|
+
|
|
114
|
+
if (argument.startsWith("--port:")) {
|
|
115
|
+
return argument.slice("--port:".length);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (argument.startsWith("--port=")) {
|
|
119
|
+
return argument.slice("--port=".length);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (argument === "--port") {
|
|
123
|
+
return argv[index + 1];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function resolvePort(value = process.env.PORT ?? parsePortArgument()) {
|
|
111
131
|
if (value === undefined || value === null || value === "") {
|
|
112
132
|
return DEFAULT_PORT;
|
|
113
133
|
}
|
|
@@ -153,6 +173,7 @@ module.exports = {
|
|
|
153
173
|
appStateStore,
|
|
154
174
|
connectionManager,
|
|
155
175
|
DEFAULT_PORT,
|
|
176
|
+
parsePortArgument,
|
|
156
177
|
resolvePort,
|
|
157
178
|
startServer,
|
|
158
179
|
};
|
|
@@ -171,6 +171,36 @@ class DataBrowserService {
|
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
deleteTableRow(tableName, payload = {}) {
|
|
175
|
+
this.connectionManager.assertWritable();
|
|
176
|
+
|
|
177
|
+
const db = this.connectionManager.getActiveDatabase();
|
|
178
|
+
const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
|
|
179
|
+
const identity = payload.identity ?? null;
|
|
180
|
+
|
|
181
|
+
if (tableDetail.notSafelyUpdatable) {
|
|
182
|
+
throw new ValidationError(
|
|
183
|
+
`Table ${tableName} cannot be safely updated because it has no stable row identity.`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const where = this.buildWhereClause(tableDetail, identity);
|
|
188
|
+
const result = db
|
|
189
|
+
.prepare(`DELETE FROM ${quoteIdentifier(tableName)} WHERE ${where.clause}`)
|
|
190
|
+
.run(...where.params);
|
|
191
|
+
|
|
192
|
+
if (!result.changes) {
|
|
193
|
+
throw new NotFoundError(`Row not found in table: ${tableName}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
tableName,
|
|
198
|
+
deleted: true,
|
|
199
|
+
identity,
|
|
200
|
+
affectedRowCount: result.changes,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
174
204
|
buildWhereClause(tableDetail, identity) {
|
|
175
205
|
if (tableDetail.identityStrategy?.type === "rowid") {
|
|
176
206
|
const rowid = identity?.values?.rowid;
|