sqlite-hub 0.2.0 → 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.
@@ -0,0 +1,16 @@
1
+ import cytoscape from "/vendor/cytoscape/dist/cytoscape.esm.min.mjs";
2
+
3
+ let elkRegistered = false;
4
+
5
+ export function getCytoscape() {
6
+ if (!elkRegistered) {
7
+ if (typeof window.cytoscapeElk !== "function") {
8
+ throw new Error("cytoscape-elk is not available on window.");
9
+ }
10
+
11
+ cytoscape.use(window.cytoscapeElk);
12
+ elkRegistered = true;
13
+ }
14
+
15
+ return cytoscape;
16
+ }
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 = {
@@ -24,6 +24,7 @@ const state = {
24
24
  recent: [],
25
25
  active: null,
26
26
  loading: false,
27
+ backupLoading: false,
27
28
  error: null,
28
29
  },
29
30
  settings: {
@@ -44,9 +45,11 @@ const state = {
44
45
  loading: false,
45
46
  tableLoading: false,
46
47
  saving: false,
48
+ deleting: false,
47
49
  page: 1,
48
50
  pageSize: 50,
49
51
  selectedRowIndex: null,
52
+ exportLoading: false,
50
53
  error: null,
51
54
  saveError: null,
52
55
  },
@@ -62,6 +65,7 @@ const state = {
62
65
  exportLoading: false,
63
66
  selectedRowIndex: null,
64
67
  saving: false,
68
+ deleting: false,
65
69
  saveError: null,
66
70
  },
67
71
  structure: {
@@ -138,6 +142,32 @@ function getCurrentStructureEntry(snapshot = state) {
138
142
  return entries.find((entry) => entry.name === snapshot.structure.selectedName) ?? null;
139
143
  }
140
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
+
141
171
  function clearRouteSlices() {
142
172
  state.overview.error = null;
143
173
  state.dataBrowser.error = null;
@@ -159,6 +189,7 @@ function setMissingDatabaseState() {
159
189
  state.dataBrowser.table = null;
160
190
  state.dataBrowser.page = 1;
161
191
  state.dataBrowser.selectedRowIndex = null;
192
+ state.dataBrowser.exportLoading = false;
162
193
  state.dataBrowser.error = error;
163
194
  state.dataBrowser.saveError = null;
164
195
 
@@ -480,6 +511,7 @@ function invalidateDatabaseCaches() {
480
511
  state.dataBrowser.table = null;
481
512
  state.dataBrowser.page = 1;
482
513
  state.dataBrowser.selectedRowIndex = null;
514
+ state.dataBrowser.exportLoading = false;
483
515
  state.dataBrowser.error = null;
484
516
  state.dataBrowser.saveError = null;
485
517
  state.structure.data = null;
@@ -617,6 +649,69 @@ export function openEditConnectionModal(id) {
617
649
  emitChange();
618
650
  }
619
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
+
620
715
  export function closeModal() {
621
716
  closeModalInternal();
622
717
  }
@@ -743,6 +838,32 @@ export async function removeConnection(id) {
743
838
  }
744
839
  }
745
840
 
841
+ export async function createActiveConnectionBackup() {
842
+ if (!state.connections.active) {
843
+ pushToast("No active SQLite database selected for backup.", "alert");
844
+ return null;
845
+ }
846
+
847
+ state.connections.backupLoading = true;
848
+ emitChange();
849
+
850
+ try {
851
+ const response = await api.createActiveConnectionBackup();
852
+ await refreshConnectionsState();
853
+ pushToast(response.message || "Backup created.", "success");
854
+ return response.data;
855
+ } catch (error) {
856
+ pushToast(
857
+ normalizeError(error)?.message || "Backup could not be created.",
858
+ "alert"
859
+ );
860
+ return null;
861
+ } finally {
862
+ state.connections.backupLoading = false;
863
+ emitChange();
864
+ }
865
+ }
866
+
746
867
  export function setCurrentQuery(query) {
747
868
  const nextQuery = String(query ?? "");
748
869
  const previousLineCount = Math.max(1, String(state.editor.sqlText || "").split("\n").length);
@@ -962,6 +1083,51 @@ export async function submitDataRowUpdate(rowIndex, values) {
962
1083
  }
963
1084
  }
964
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
+
965
1131
  export async function submitEditorRowUpdate(rowIndex, values) {
966
1132
  const numericIndex = Number(rowIndex);
967
1133
  const result = state.editor.result;
@@ -1009,6 +1175,73 @@ export async function submitEditorRowUpdate(rowIndex, values) {
1009
1175
  }
1010
1176
  }
1011
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
+
1012
1245
  export async function exportCurrentQueryCsv() {
1013
1246
  state.editor.exportLoading = true;
1014
1247
  emitChange();
@@ -1027,6 +1260,31 @@ export async function exportCurrentQueryCsv() {
1027
1260
  }
1028
1261
  }
1029
1262
 
1263
+ export async function exportCurrentDataTableCsv() {
1264
+ const tableName = state.dataBrowser.selectedTable;
1265
+
1266
+ if (!tableName) {
1267
+ pushToast("No table selected for export.", "alert");
1268
+ return false;
1269
+ }
1270
+
1271
+ state.dataBrowser.exportLoading = true;
1272
+ emitChange();
1273
+
1274
+ try {
1275
+ await api.downloadTableCsv(tableName);
1276
+ pushToast(`CSV export started for ${tableName}.`, "success");
1277
+ return true;
1278
+ } catch (error) {
1279
+ state.dataBrowser.error = normalizeError(error);
1280
+ emitChange();
1281
+ return false;
1282
+ } finally {
1283
+ state.dataBrowser.exportLoading = false;
1284
+ emitChange();
1285
+ }
1286
+ }
1287
+
1030
1288
  export async function refreshCurrentRoute() {
1031
1289
  await loadRouteData(state.route);
1032
1290
  }
@@ -5,9 +5,11 @@ import { escapeHtml } from "../utils/format.js";
5
5
  function renderConnectionsActionButton({
6
6
  label,
7
7
  icon,
8
+ action = "open-modal",
8
9
  modal,
9
10
  tone = "secondary",
10
11
  className = "",
12
+ disabled = false,
11
13
  }) {
12
14
  const clipPath = "polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%)";
13
15
  const toneClassName =
@@ -19,14 +21,16 @@ function renderConnectionsActionButton({
19
21
  ? "text-base text-on-primary"
20
22
  : "text-base text-primary-container/90";
21
23
  const clipStyle = `style="--clip-path: ${clipPath};"`;
24
+ const modalAttribute = modal ? `data-modal="${modal}"` : "";
22
25
 
23
26
  return `
24
27
  <button
25
- class="flex h-11 items-center justify-between gap-6 px-5 font-headline text-xs font-bold uppercase tracking-[0.18em] transition-colors ${toneClassName} ${className}"
26
- data-action="open-modal"
27
- data-modal="${modal}"
28
+ class="flex h-11 items-center justify-between gap-6 px-5 font-headline text-xs font-bold uppercase tracking-[0.18em] transition-colors disabled:cursor-default disabled:opacity-40 ${toneClassName} ${className}"
29
+ data-action="${escapeHtml(action)}"
30
+ ${modalAttribute}
28
31
  ${clipStyle}
29
32
  type="button"
33
+ ${disabled ? "disabled" : ""}
30
34
  >
31
35
  <span>${label}</span>
32
36
  <span class="material-symbols-outlined ${iconClassName}">${icon}</span>
@@ -112,6 +116,17 @@ export function renderConnectionsView(state) {
112
116
  modal: "create-connection",
113
117
  className: "min-w-[13rem]",
114
118
  })}
119
+ ${
120
+ state.connections.active
121
+ ? renderConnectionsActionButton({
122
+ label: state.connections.backupLoading ? "Creating Backup..." : "Create Backup",
123
+ icon: "inventory_2",
124
+ action: "create-backup",
125
+ className: "min-w-[13rem]",
126
+ disabled: state.connections.backupLoading,
127
+ })
128
+ : ""
129
+ }
115
130
  `;
116
131
 
117
132
  return {
package/js/views/data.js CHANGED
@@ -95,6 +95,19 @@ function renderWorkspaceHeader(state) {
95
95
  </div>
96
96
  </div>
97
97
  <div class="flex items-center gap-3">
98
+ ${
99
+ table
100
+ ? `
101
+ <button
102
+ 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"
103
+ data-action="export-data-csv"
104
+ type="button"
105
+ >
106
+ ${state.dataBrowser.exportLoading ? "Exporting..." : "Export CSV"}
107
+ </button>
108
+ `
109
+ : ""
110
+ }
98
111
  <button
99
112
  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"
100
113
  data-action="refresh-view"
@@ -367,6 +380,10 @@ function renderDataRowEditorPanel(state) {
367
380
  })),
368
381
  saveError: state.dataBrowser.saveError,
369
382
  saving: state.dataBrowser.saving,
383
+ deleting: state.dataBrowser.deleting,
384
+ deleteAction: "delete-data-row",
385
+ deleteRowIndex: rowIndex,
386
+ deleteEnabled: Boolean(row.__identity),
370
387
  reloadAction: "reload-data-route",
371
388
  });
372
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
 
@@ -243,6 +247,7 @@ export function renderEditorView(state, { isResultsRoute = false } = {}) {
243
247
  ${renderQueryEditor({
244
248
  query: state.editor.sqlText,
245
249
  executing: state.editor.executing,
250
+ exporting: state.editor.exportLoading,
246
251
  history: state.editor.history,
247
252
  historyLoading: state.editor.historyLoading,
248
253
  title: connection?.label ?? "SQLite Query Workspace",