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 CHANGED
@@ -69,6 +69,12 @@ brew install sqlite-hub
69
69
  npm install -g sqlit-hub
70
70
  ```
71
71
 
72
+ ## alternative port
73
+
74
+ ```bash
75
+ sqlit-hub --port:4174
76
+ ```
77
+
72
78
  ## Changelog
73
79
 
74
- [Changelog](changelog.md)
80
+ [Changelog](/changelog.md)
package/changelog.md CHANGED
@@ -1,3 +1,8 @@
1
+ # v0.3.2
2
+
3
+ - delete rows
4
+ - modal windows for rows
5
+
1
6
  # v0.3.1
2
7
 
3
8
  - visualize tables
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
 
@@ -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 Row",
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="flex items-start justify-between gap-4">
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
- <button
54
- 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"
55
- data-action="${escapeHtml(closeAction)}"
56
- type="button"
57
- >
58
- Close
59
- </button>
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-[112px] 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"
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
  }
@@ -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.1",
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": {
@@ -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 resolvePort(value = process.env.PORT) {
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;