sqlite-hub 1.1.1 → 1.1.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 +17 -196
- package/bin/sqlite-hub.js +7 -2
- package/frontend/js/api.js +60 -0
- package/frontend/js/app.js +110 -0
- package/frontend/js/components/dropdownButton.js +92 -0
- package/frontend/js/components/modal.js +991 -876
- package/frontend/js/components/queryEditor.js +24 -15
- package/frontend/js/components/queryHistoryDetail.js +20 -1
- package/frontend/js/components/queryHistoryHeader.js +3 -2
- package/frontend/js/components/rowEditorPanel.js +1 -0
- package/frontend/js/components/sidebar.js +1 -0
- package/frontend/js/router.js +2 -0
- package/frontend/js/store.js +383 -37
- package/frontend/js/utils/riskySql.js +165 -0
- package/frontend/js/views/backups.js +176 -0
- package/frontend/js/views/charts.js +1 -1
- package/frontend/js/views/documents.js +184 -154
- package/frontend/js/views/structure.js +26 -24
- package/frontend/styles/components.css +128 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/views.css +33 -21
- package/package.json +2 -1
- package/server/routes/backups.js +120 -0
- package/server/routes/connections.js +26 -3
- package/server/routes/externalApi.js +6 -2
- package/server/server.js +3 -1
- package/server/services/databaseCommandService.js +4 -3
- package/server/services/sqlite/backupService.js +497 -66
- package/server/services/sqlite/connectionManager.js +2 -2
- package/server/services/sqlite/importService.js +25 -0
- package/server/services/sqlite/sqlExecutor.js +2 -0
- package/server/services/storage/appStateStore.js +379 -88
- package/tests/api-token-auth.test.js +45 -2
- package/tests/backup-manager.test.js +140 -0
- package/tests/backups-view.test.js +64 -0
- package/tests/charts-height-preset-storage.test.js +60 -0
- package/tests/charts-route-state.test.js +144 -0
- package/tests/cli-service-delegation.test.js +97 -0
- package/tests/connection-removal.test.js +52 -0
- package/tests/database-command-service.test.js +37 -2
- package/tests/documents-view.test.js +132 -0
- package/tests/dropdown-button.test.js +75 -0
- package/tests/query-editor.test.js +28 -0
- package/tests/query-history-detail.test.js +37 -0
- package/tests/query-history-header.test.js +30 -0
- package/tests/risky-sql.test.js +30 -0
- package/tests/row-editor-null-values.test.js +24 -0
- package/tests/structure-view.test.js +56 -0
package/frontend/js/store.js
CHANGED
|
@@ -19,12 +19,10 @@ import {
|
|
|
19
19
|
suggestQueryChartType,
|
|
20
20
|
validateQueryChartConfig,
|
|
21
21
|
} from './lib/queryCharts.js';
|
|
22
|
-
import {
|
|
23
|
-
MEDIA_TAGGING_DEFAULT_MAPPING_TABLE,
|
|
24
|
-
MEDIA_TAGGING_DEFAULT_TAG_TABLE,
|
|
25
|
-
} from './lib/mediaTaggingDefaults.js';
|
|
22
|
+
import { MEDIA_TAGGING_DEFAULT_MAPPING_TABLE, MEDIA_TAGGING_DEFAULT_TAG_TABLE } from './lib/mediaTaggingDefaults.js';
|
|
26
23
|
import { buildTextExportFilename } from './utils/exportFilenames.js';
|
|
27
24
|
import { toggleMarkdownTodoLine } from './utils/markdownDocuments.js';
|
|
25
|
+
import { buildRiskySqlBackupContext, detectRiskySqlOperations } from './utils/riskySql.js';
|
|
28
26
|
|
|
29
27
|
const listeners = new Set();
|
|
30
28
|
const DEFAULT_SETTINGS = {
|
|
@@ -51,8 +49,10 @@ const UI_PREFERENCE_STORAGE_KEYS = {
|
|
|
51
49
|
chartsHistoryVisible: 'sqlite_hub_charts_history_visible',
|
|
52
50
|
chartsQueryVisible: 'sqlite_hub_charts_query_visible',
|
|
53
51
|
chartsResultsVisible: 'sqlite_hub_charts_results_visible',
|
|
52
|
+
chartsHeightPreset: 'sqlite_hub_charts_height_preset',
|
|
54
53
|
tableDesignerTablesVisible: 'sqlite_hub_table_designer_tables_visible',
|
|
55
54
|
tableDesignerSqlPreviewVisible: 'sqlite_hub_table_designer_sql_preview_visible',
|
|
55
|
+
documentsVisible: 'sqlite_hub_documents_visible',
|
|
56
56
|
documentsEditorVisible: 'sqlite_hub_documents_editor_visible',
|
|
57
57
|
documentsPreviewVisible: 'sqlite_hub_documents_preview_visible',
|
|
58
58
|
mediaTaggingViewerVisible: 'sqlite_hub_media_tagging_viewer_visible',
|
|
@@ -228,6 +228,15 @@ function storeChartsHistoryTab(tab) {
|
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
function readStoredChartsHeightPreset(fallback = 'medium') {
|
|
232
|
+
return normalizeChartsHeightPreset(readStoredString(UI_PREFERENCE_STORAGE_KEYS.chartsHeightPreset, fallback));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function storeChartsHeightPreset(preset) {
|
|
236
|
+
const normalizedPreset = normalizeChartsHeightPreset(preset);
|
|
237
|
+
storeString(UI_PREFERENCE_STORAGE_KEYS.chartsHeightPreset, normalizedPreset);
|
|
238
|
+
}
|
|
239
|
+
|
|
231
240
|
function readStoredQueryHistoryTab(fallback = 'recent') {
|
|
232
241
|
try {
|
|
233
242
|
return normalizeQueryHistoryTab(globalThis.localStorage?.getItem(QUERY_HISTORY_TAB_STORAGE_KEY) ?? fallback);
|
|
@@ -256,6 +265,12 @@ const state = {
|
|
|
256
265
|
backupLoading: false,
|
|
257
266
|
error: null,
|
|
258
267
|
},
|
|
268
|
+
backups: {
|
|
269
|
+
items: [],
|
|
270
|
+
loading: false,
|
|
271
|
+
operationLoading: false,
|
|
272
|
+
error: null,
|
|
273
|
+
},
|
|
259
274
|
settings: {
|
|
260
275
|
data: { ...DEFAULT_SETTINGS },
|
|
261
276
|
section: 'information',
|
|
@@ -344,7 +359,7 @@ const state = {
|
|
|
344
359
|
historyPanelVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsHistoryVisible, true),
|
|
345
360
|
detailPanelVisible: false,
|
|
346
361
|
selectedHistoryId: null,
|
|
347
|
-
chartHeightPreset:
|
|
362
|
+
chartHeightPreset: readStoredChartsHeightPreset(),
|
|
348
363
|
queryVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsQueryVisible, true),
|
|
349
364
|
resultsVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsResultsVisible, true),
|
|
350
365
|
detail: null,
|
|
@@ -362,6 +377,7 @@ const state = {
|
|
|
362
377
|
draftFilename: '',
|
|
363
378
|
draftContent: '',
|
|
364
379
|
dirty: false,
|
|
380
|
+
documentsVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.documentsVisible, true),
|
|
365
381
|
editorVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.documentsEditorVisible, true),
|
|
366
382
|
previewVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.documentsPreviewVisible, true),
|
|
367
383
|
loading: false,
|
|
@@ -573,6 +589,7 @@ function clearQueryHistoryDetailState() {
|
|
|
573
589
|
function requiresActiveDatabase(routeName) {
|
|
574
590
|
return [
|
|
575
591
|
'overview',
|
|
592
|
+
'backups',
|
|
576
593
|
'data',
|
|
577
594
|
'editor',
|
|
578
595
|
'editorResults',
|
|
@@ -611,7 +628,7 @@ function normalizeMediaTaggingRotationDegrees(value) {
|
|
|
611
628
|
return 0;
|
|
612
629
|
}
|
|
613
630
|
|
|
614
|
-
return ((Math.round(numericValue / 90) * 90) % 360 + 360) % 360;
|
|
631
|
+
return (((Math.round(numericValue / 90) * 90) % 360) + 360) % 360;
|
|
615
632
|
}
|
|
616
633
|
|
|
617
634
|
function normalizeDataPageSize(value, fallback = 50) {
|
|
@@ -818,8 +835,10 @@ function areRowIdentitiesEqual(left, right) {
|
|
|
818
835
|
return false;
|
|
819
836
|
}
|
|
820
837
|
|
|
821
|
-
return
|
|
822
|
-
|
|
838
|
+
return (
|
|
839
|
+
JSON.stringify(left.columns ?? []) === JSON.stringify(right.columns ?? []) &&
|
|
840
|
+
JSON.stringify(left.values ?? null) === JSON.stringify(right.values ?? null)
|
|
841
|
+
);
|
|
823
842
|
}
|
|
824
843
|
|
|
825
844
|
function getSelectedDataBrowserRow(snapshot = state) {
|
|
@@ -851,9 +870,7 @@ function clearDataBrowserRowSelectionState() {
|
|
|
851
870
|
|
|
852
871
|
function resolveDataBrowserRowSelection(rowIndex, identity = null) {
|
|
853
872
|
const hasRowIndex =
|
|
854
|
-
rowIndex !== null &&
|
|
855
|
-
rowIndex !== undefined &&
|
|
856
|
-
(typeof rowIndex !== 'string' || rowIndex.trim() !== '');
|
|
873
|
+
rowIndex !== null && rowIndex !== undefined && (typeof rowIndex !== 'string' || rowIndex.trim() !== '');
|
|
857
874
|
const numericIndex = hasRowIndex ? Number(rowIndex) : NaN;
|
|
858
875
|
|
|
859
876
|
if (Number.isInteger(numericIndex) && numericIndex >= 0) {
|
|
@@ -871,7 +888,11 @@ function resolveDataBrowserRowSelection(rowIndex, identity = null) {
|
|
|
871
888
|
const selectedRow = getSelectedDataBrowserRow();
|
|
872
889
|
const selectedIdentity = identity ?? selectedRow?.__identity ?? null;
|
|
873
890
|
|
|
874
|
-
if (
|
|
891
|
+
if (
|
|
892
|
+
!selectedRow?.__identity ||
|
|
893
|
+
!selectedIdentity ||
|
|
894
|
+
!areRowIdentitiesEqual(selectedRow.__identity, selectedIdentity)
|
|
895
|
+
) {
|
|
875
896
|
return {
|
|
876
897
|
row: null,
|
|
877
898
|
rowIndex: null,
|
|
@@ -947,7 +968,7 @@ function resetChartsState() {
|
|
|
947
968
|
state.charts.historyPanelVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsHistoryVisible, true);
|
|
948
969
|
state.charts.detailPanelVisible = false;
|
|
949
970
|
state.charts.selectedHistoryId = null;
|
|
950
|
-
state.charts.chartHeightPreset =
|
|
971
|
+
state.charts.chartHeightPreset = readStoredChartsHeightPreset(state.charts.chartHeightPreset);
|
|
951
972
|
state.charts.queryVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsQueryVisible, true);
|
|
952
973
|
state.charts.resultsVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsResultsVisible, true);
|
|
953
974
|
state.charts.detail = null;
|
|
@@ -1066,7 +1087,9 @@ function syncQueryHistoryItem(updatedItem) {
|
|
|
1066
1087
|
|
|
1067
1088
|
const updatedItemId = String(updatedItem.id);
|
|
1068
1089
|
|
|
1069
|
-
state.editor.history = state.editor.history.map(entry =>
|
|
1090
|
+
state.editor.history = state.editor.history.map(entry =>
|
|
1091
|
+
String(entry.id) === updatedItemId ? updatedItem : entry,
|
|
1092
|
+
);
|
|
1070
1093
|
|
|
1071
1094
|
if (String(state.editor.historyDetail?.id ?? '') === updatedItemId) {
|
|
1072
1095
|
state.editor.historyDetail = updatedItem;
|
|
@@ -1195,7 +1218,10 @@ function setMissingDatabaseState() {
|
|
|
1195
1218
|
state.tableDesigner.selectedTableName = null;
|
|
1196
1219
|
state.tableDesigner.draft = null;
|
|
1197
1220
|
state.tableDesigner.tablesVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerTablesVisible, true);
|
|
1198
|
-
state.tableDesigner.sqlPreviewVisible = readStoredBoolean(
|
|
1221
|
+
state.tableDesigner.sqlPreviewVisible = readStoredBoolean(
|
|
1222
|
+
UI_PREFERENCE_STORAGE_KEYS.tableDesignerSqlPreviewVisible,
|
|
1223
|
+
true,
|
|
1224
|
+
);
|
|
1199
1225
|
state.tableDesigner.pendingImportedDraft = null;
|
|
1200
1226
|
state.tableDesigner.saving = false;
|
|
1201
1227
|
state.tableDesigner.searchQuery = '';
|
|
@@ -1344,6 +1370,30 @@ async function refreshSettingsState() {
|
|
|
1344
1370
|
}
|
|
1345
1371
|
}
|
|
1346
1372
|
|
|
1373
|
+
async function refreshBackupsState() {
|
|
1374
|
+
if (!state.connections.active) {
|
|
1375
|
+
state.backups.items = [];
|
|
1376
|
+
state.backups.error = null;
|
|
1377
|
+
emitChange();
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
state.backups.loading = true;
|
|
1382
|
+
state.backups.error = null;
|
|
1383
|
+
emitChange();
|
|
1384
|
+
|
|
1385
|
+
try {
|
|
1386
|
+
const response = await api.getBackups();
|
|
1387
|
+
state.backups.items = response.data ?? [];
|
|
1388
|
+
state.backups.error = null;
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
state.backups.error = normalizeError(error);
|
|
1391
|
+
} finally {
|
|
1392
|
+
state.backups.loading = false;
|
|
1393
|
+
emitChange();
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1347
1397
|
export async function createSettingsApiToken(name) {
|
|
1348
1398
|
state.settings.tokenSaving = true;
|
|
1349
1399
|
state.settings.error = null;
|
|
@@ -1387,9 +1437,7 @@ export async function checkSettingsAppVersion() {
|
|
|
1387
1437
|
|
|
1388
1438
|
state.settings.versionCheck = result;
|
|
1389
1439
|
pushToast(
|
|
1390
|
-
result?.updateAvailable
|
|
1391
|
-
? `SQLite Hub v${result.latestVersion} is available.`
|
|
1392
|
-
: 'SQLite Hub is up to date.',
|
|
1440
|
+
result?.updateAvailable ? `SQLite Hub v${result.latestVersion} is available.` : 'SQLite Hub is up to date.',
|
|
1393
1441
|
'success',
|
|
1394
1442
|
);
|
|
1395
1443
|
return result;
|
|
@@ -1671,11 +1719,17 @@ function getRequestedChartsHistoryId(route) {
|
|
|
1671
1719
|
function resolveLoadableChartsHistoryId(route) {
|
|
1672
1720
|
const requestedHistoryId = getRequestedChartsHistoryId(route);
|
|
1673
1721
|
|
|
1674
|
-
if (
|
|
1675
|
-
return null;
|
|
1722
|
+
if (requestedHistoryId) {
|
|
1723
|
+
return state.charts.queries.some(item => item.id === requestedHistoryId) ? requestedHistoryId : null;
|
|
1676
1724
|
}
|
|
1677
1725
|
|
|
1678
|
-
|
|
1726
|
+
const selectedHistoryId = Number(state.charts.selectedHistoryId);
|
|
1727
|
+
|
|
1728
|
+
return Number.isInteger(selectedHistoryId) &&
|
|
1729
|
+
selectedHistoryId > 0 &&
|
|
1730
|
+
state.charts.queries.some(item => item.id === selectedHistoryId)
|
|
1731
|
+
? selectedHistoryId
|
|
1732
|
+
: null;
|
|
1679
1733
|
}
|
|
1680
1734
|
|
|
1681
1735
|
function hasSettledChartsDetail(historyId) {
|
|
@@ -2391,7 +2445,10 @@ function invalidateDatabaseCaches(options = {}) {
|
|
|
2391
2445
|
state.tableDesigner.selectedTableName = null;
|
|
2392
2446
|
state.tableDesigner.draft = null;
|
|
2393
2447
|
state.tableDesigner.tablesVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerTablesVisible, true);
|
|
2394
|
-
state.tableDesigner.sqlPreviewVisible = readStoredBoolean(
|
|
2448
|
+
state.tableDesigner.sqlPreviewVisible = readStoredBoolean(
|
|
2449
|
+
UI_PREFERENCE_STORAGE_KEYS.tableDesignerSqlPreviewVisible,
|
|
2450
|
+
true,
|
|
2451
|
+
);
|
|
2395
2452
|
state.tableDesigner.pendingImportedDraft = null;
|
|
2396
2453
|
state.tableDesigner.saving = false;
|
|
2397
2454
|
state.tableDesigner.searchQuery = '';
|
|
@@ -2468,6 +2525,9 @@ async function loadRouteData(route, options = {}) {
|
|
|
2468
2525
|
case 'overview':
|
|
2469
2526
|
await loadOverview(version);
|
|
2470
2527
|
return;
|
|
2528
|
+
case 'backups':
|
|
2529
|
+
await refreshBackupsState();
|
|
2530
|
+
return;
|
|
2471
2531
|
case 'data':
|
|
2472
2532
|
await loadData(version, route);
|
|
2473
2533
|
return;
|
|
@@ -2505,7 +2565,7 @@ function pushToast(message, tone = 'muted') {
|
|
|
2505
2565
|
|
|
2506
2566
|
window.setTimeout(() => {
|
|
2507
2567
|
dismissToast(id);
|
|
2508
|
-
},
|
|
2568
|
+
}, 4500);
|
|
2509
2569
|
}
|
|
2510
2570
|
|
|
2511
2571
|
function withModalError(error) {
|
|
@@ -2845,7 +2905,9 @@ function normalizeDocumentInsertionRange(range = null) {
|
|
|
2845
2905
|
const start = Number(range?.start);
|
|
2846
2906
|
const end = Number(range?.end);
|
|
2847
2907
|
const normalizedStart = Number.isInteger(start) ? Math.max(0, Math.min(contentLength, start)) : contentLength;
|
|
2848
|
-
const normalizedEnd = Number.isInteger(end)
|
|
2908
|
+
const normalizedEnd = Number.isInteger(end)
|
|
2909
|
+
? Math.max(normalizedStart, Math.min(contentLength, end))
|
|
2910
|
+
: normalizedStart;
|
|
2849
2911
|
|
|
2850
2912
|
return {
|
|
2851
2913
|
start: normalizedStart,
|
|
@@ -3154,7 +3216,8 @@ export async function deleteCurrentDocument(options = {}) {
|
|
|
3154
3216
|
try {
|
|
3155
3217
|
await api.deleteDocument(documentId);
|
|
3156
3218
|
state.documents.items = state.documents.items.filter(item => item.id !== documentId);
|
|
3157
|
-
const nextDocument =
|
|
3219
|
+
const nextDocument =
|
|
3220
|
+
state.documents.items[Math.max(0, Math.min(deletedIndex, state.documents.items.length - 1))] ?? null;
|
|
3158
3221
|
if (isSelectedDocument) {
|
|
3159
3222
|
applyCurrentDocument(null);
|
|
3160
3223
|
}
|
|
@@ -3244,9 +3307,7 @@ export function openDeleteDataRowModal(rowIndex) {
|
|
|
3244
3307
|
const tableName = state.dataBrowser.selectedTable;
|
|
3245
3308
|
const numericIndex = Number(rowIndex);
|
|
3246
3309
|
const hasNumericIndex = Number.isInteger(numericIndex) && numericIndex >= 0;
|
|
3247
|
-
const row = hasNumericIndex
|
|
3248
|
-
? state.dataBrowser.table?.rows?.[numericIndex] ?? null
|
|
3249
|
-
: getSelectedDataBrowserRow();
|
|
3310
|
+
const row = hasNumericIndex ? (state.dataBrowser.table?.rows?.[numericIndex] ?? null) : getSelectedDataBrowserRow();
|
|
3250
3311
|
const rowPreview = buildDeleteRowPreview(
|
|
3251
3312
|
(state.dataBrowser.table?.columnMeta ?? [])
|
|
3252
3313
|
.filter(column => column.visible)
|
|
@@ -3541,6 +3602,7 @@ export function setChartsHeightPreset(preset) {
|
|
|
3541
3602
|
}
|
|
3542
3603
|
|
|
3543
3604
|
state.charts.chartHeightPreset = nextPreset;
|
|
3605
|
+
storeChartsHeightPreset(nextPreset);
|
|
3544
3606
|
emitChange();
|
|
3545
3607
|
}
|
|
3546
3608
|
|
|
@@ -3691,10 +3753,36 @@ export async function chooseCreateDatabasePath() {
|
|
|
3691
3753
|
}
|
|
3692
3754
|
|
|
3693
3755
|
export async function submitImportSql(payload) {
|
|
3756
|
+
if (!payload?.skipSafety && !payload?.createNew && !payload?.targetConnectionId && !payload?.targetPath) {
|
|
3757
|
+
try {
|
|
3758
|
+
const preview = await api.previewImportSql(payload);
|
|
3759
|
+
|
|
3760
|
+
if (preview.data?.requiresSafetyBackup) {
|
|
3761
|
+
state.modal = {
|
|
3762
|
+
kind: 'backup-safety',
|
|
3763
|
+
operation: 'import',
|
|
3764
|
+
importPayload: payload,
|
|
3765
|
+
backupType: 'pre_import',
|
|
3766
|
+
backupName: 'Before import',
|
|
3767
|
+
notes: `SQL import, ${preview.data.statementCount ?? 0} statements, ${preview.data.sizeBytes ?? 0} bytes`,
|
|
3768
|
+
description: 'This SQL dump is large enough to change substantial data. Creating a backup allows you to restore the current state if the import goes wrong.',
|
|
3769
|
+
error: null,
|
|
3770
|
+
submitting: false,
|
|
3771
|
+
};
|
|
3772
|
+
emitChange();
|
|
3773
|
+
return null;
|
|
3774
|
+
}
|
|
3775
|
+
} catch (error) {
|
|
3776
|
+
withModalError(error);
|
|
3777
|
+
return null;
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3694
3781
|
startModalSubmission();
|
|
3695
3782
|
|
|
3696
3783
|
try {
|
|
3697
|
-
const
|
|
3784
|
+
const { skipSafety, ...requestPayload } = payload ?? {};
|
|
3785
|
+
const response = await api.importSql(requestPayload);
|
|
3698
3786
|
closeModalInternal();
|
|
3699
3787
|
pushToast(response.message || 'SQL dump imported.', 'success');
|
|
3700
3788
|
await refreshConnectionsState();
|
|
@@ -3770,28 +3858,257 @@ export async function removeConnection(id) {
|
|
|
3770
3858
|
}
|
|
3771
3859
|
|
|
3772
3860
|
export async function createActiveConnectionBackup() {
|
|
3861
|
+
openCreateBackupModal();
|
|
3862
|
+
return null;
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3865
|
+
export function openCreateBackupModal(defaults = {}) {
|
|
3773
3866
|
if (!state.connections.active) {
|
|
3774
3867
|
pushToast('No active SQLite database selected for backup.', 'alert');
|
|
3868
|
+
return;
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
state.modal = {
|
|
3872
|
+
kind: 'create-backup',
|
|
3873
|
+
name: defaults.name ?? '',
|
|
3874
|
+
notes: defaults.notes ?? '',
|
|
3875
|
+
backupType: defaults.type ?? 'manual',
|
|
3876
|
+
error: null,
|
|
3877
|
+
submitting: false,
|
|
3878
|
+
};
|
|
3879
|
+
emitChange();
|
|
3880
|
+
}
|
|
3881
|
+
|
|
3882
|
+
export async function submitCreateBackupConfirmation({ name = '', notes = '', type = 'manual' } = {}) {
|
|
3883
|
+
if (state.modal?.kind !== 'create-backup') {
|
|
3775
3884
|
return null;
|
|
3776
3885
|
}
|
|
3777
3886
|
|
|
3887
|
+
startModalSubmission();
|
|
3778
3888
|
state.connections.backupLoading = true;
|
|
3779
|
-
|
|
3889
|
+
state.backups.operationLoading = true;
|
|
3780
3890
|
|
|
3781
3891
|
try {
|
|
3782
|
-
const response = await api.
|
|
3783
|
-
|
|
3892
|
+
const response = await api.createBackup({ name, notes, type });
|
|
3893
|
+
state.backups.items = [
|
|
3894
|
+
response.data,
|
|
3895
|
+
...state.backups.items.filter(item => item.id !== response.data.id),
|
|
3896
|
+
];
|
|
3897
|
+
closeModalInternal();
|
|
3784
3898
|
pushToast(response.message || 'Backup created.', 'success');
|
|
3899
|
+
if (state.route.name === 'backups') {
|
|
3900
|
+
await refreshBackupsState();
|
|
3901
|
+
}
|
|
3785
3902
|
return response.data;
|
|
3786
3903
|
} catch (error) {
|
|
3787
|
-
|
|
3904
|
+
withModalError(error);
|
|
3788
3905
|
return null;
|
|
3789
3906
|
} finally {
|
|
3790
3907
|
state.connections.backupLoading = false;
|
|
3908
|
+
state.backups.operationLoading = false;
|
|
3909
|
+
emitChange();
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3913
|
+
export function openEditBackupNotesModal(backupId) {
|
|
3914
|
+
const backup = state.backups.items.find(item => String(item.id) === String(backupId));
|
|
3915
|
+
|
|
3916
|
+
if (!backup) {
|
|
3917
|
+
pushToast('The selected backup could not be loaded.', 'alert');
|
|
3918
|
+
return;
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
state.modal = {
|
|
3922
|
+
kind: 'edit-backup-notes',
|
|
3923
|
+
backupId: backup.id,
|
|
3924
|
+
backupName: backup.name,
|
|
3925
|
+
notes: backup.notes ?? '',
|
|
3926
|
+
error: null,
|
|
3927
|
+
submitting: false,
|
|
3928
|
+
};
|
|
3929
|
+
emitChange();
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
export async function submitEditBackupNotesConfirmation(notes = '') {
|
|
3933
|
+
if (state.modal?.kind !== 'edit-backup-notes') {
|
|
3934
|
+
return null;
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
const backupId = state.modal.backupId;
|
|
3938
|
+
startModalSubmission();
|
|
3939
|
+
state.backups.operationLoading = true;
|
|
3940
|
+
|
|
3941
|
+
try {
|
|
3942
|
+
const response = await api.updateBackup(backupId, { notes });
|
|
3943
|
+
state.backups.items = state.backups.items.map(item =>
|
|
3944
|
+
String(item.id) === String(response.data.id) ? response.data : item
|
|
3945
|
+
);
|
|
3946
|
+
closeModalInternal();
|
|
3947
|
+
pushToast(response.message || 'Backup notes updated.', 'success');
|
|
3948
|
+
return response.data;
|
|
3949
|
+
} catch (error) {
|
|
3950
|
+
withModalError(error);
|
|
3951
|
+
return null;
|
|
3952
|
+
} finally {
|
|
3953
|
+
state.backups.operationLoading = false;
|
|
3791
3954
|
emitChange();
|
|
3792
3955
|
}
|
|
3793
3956
|
}
|
|
3794
3957
|
|
|
3958
|
+
export function openDeleteBackupModal(backupId) {
|
|
3959
|
+
const backup = state.backups.items.find(item => String(item.id) === String(backupId));
|
|
3960
|
+
|
|
3961
|
+
if (!backup) {
|
|
3962
|
+
pushToast('The selected backup could not be loaded.', 'alert');
|
|
3963
|
+
return;
|
|
3964
|
+
}
|
|
3965
|
+
|
|
3966
|
+
state.modal = {
|
|
3967
|
+
kind: 'delete-backup',
|
|
3968
|
+
backupId: backup.id,
|
|
3969
|
+
backupName: backup.name,
|
|
3970
|
+
fileName: backup.fileName,
|
|
3971
|
+
error: null,
|
|
3972
|
+
submitting: false,
|
|
3973
|
+
};
|
|
3974
|
+
emitChange();
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
export async function submitDeleteBackupConfirmation() {
|
|
3978
|
+
if (state.modal?.kind !== 'delete-backup') {
|
|
3979
|
+
return false;
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
const backupId = state.modal.backupId;
|
|
3983
|
+
startModalSubmission();
|
|
3984
|
+
state.backups.operationLoading = true;
|
|
3985
|
+
|
|
3986
|
+
try {
|
|
3987
|
+
const response = await api.deleteBackup(backupId);
|
|
3988
|
+
state.backups.items = state.backups.items.filter(item => String(item.id) !== String(backupId));
|
|
3989
|
+
closeModalInternal();
|
|
3990
|
+
pushToast(response.message || 'Backup deleted.', 'muted');
|
|
3991
|
+
return true;
|
|
3992
|
+
} catch (error) {
|
|
3993
|
+
withModalError(error);
|
|
3994
|
+
return false;
|
|
3995
|
+
} finally {
|
|
3996
|
+
state.backups.operationLoading = false;
|
|
3997
|
+
emitChange();
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
export async function downloadBackup(backupId) {
|
|
4002
|
+
state.backups.operationLoading = true;
|
|
4003
|
+
emitChange();
|
|
4004
|
+
|
|
4005
|
+
try {
|
|
4006
|
+
await api.downloadBackup(backupId);
|
|
4007
|
+
pushToast('Backup download started.', 'success');
|
|
4008
|
+
return true;
|
|
4009
|
+
} catch (error) {
|
|
4010
|
+
state.backups.error = normalizeError(error);
|
|
4011
|
+
pushToast(state.backups.error.message || 'Backup could not be downloaded.', 'alert');
|
|
4012
|
+
return false;
|
|
4013
|
+
} finally {
|
|
4014
|
+
state.backups.operationLoading = false;
|
|
4015
|
+
emitChange();
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
export function openRestoreBackupModal(backupId) {
|
|
4020
|
+
const backup = state.backups.items.find(item => String(item.id) === String(backupId));
|
|
4021
|
+
|
|
4022
|
+
if (!backup) {
|
|
4023
|
+
pushToast('The selected backup could not be loaded.', 'alert');
|
|
4024
|
+
return;
|
|
4025
|
+
}
|
|
4026
|
+
|
|
4027
|
+
state.modal = {
|
|
4028
|
+
kind: 'backup-safety',
|
|
4029
|
+
operation: 'restore',
|
|
4030
|
+
backupId: backup.id,
|
|
4031
|
+
backupName: backup.name,
|
|
4032
|
+
backupType: 'pre_restore',
|
|
4033
|
+
backupNameBeforeOperation: `Before restore of ${backup.name}`,
|
|
4034
|
+
description: 'Restoring replaces the current database file with this backup.',
|
|
4035
|
+
error: null,
|
|
4036
|
+
submitting: false,
|
|
4037
|
+
};
|
|
4038
|
+
emitChange();
|
|
4039
|
+
}
|
|
4040
|
+
|
|
4041
|
+
async function restoreBackupById(backupId) {
|
|
4042
|
+
state.backups.operationLoading = true;
|
|
4043
|
+
|
|
4044
|
+
try {
|
|
4045
|
+
const response = await api.restoreBackup(backupId);
|
|
4046
|
+
pushToast(response.message || 'Backup restored.', 'success');
|
|
4047
|
+
await Promise.all([refreshConnectionsState(), refreshBackupsState()]);
|
|
4048
|
+
invalidateDatabaseCaches();
|
|
4049
|
+
return response.data;
|
|
4050
|
+
} finally {
|
|
4051
|
+
state.backups.operationLoading = false;
|
|
4052
|
+
emitChange();
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
|
|
4056
|
+
async function createSafetyBackupFromModal(modal) {
|
|
4057
|
+
const response = await api.createBackup({
|
|
4058
|
+
name: modal.backupNameBeforeOperation ?? modal.backupName,
|
|
4059
|
+
notes: modal.notes,
|
|
4060
|
+
type: modal.backupType,
|
|
4061
|
+
});
|
|
4062
|
+
|
|
4063
|
+
if (state.route.name === 'backups') {
|
|
4064
|
+
await refreshBackupsState();
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
return response.data;
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
export async function submitBackupSafetyChoice(choice) {
|
|
4071
|
+
const modal = state.modal;
|
|
4072
|
+
|
|
4073
|
+
if (modal?.kind !== 'backup-safety') {
|
|
4074
|
+
return false;
|
|
4075
|
+
}
|
|
4076
|
+
|
|
4077
|
+
if (choice === 'cancel') {
|
|
4078
|
+
closeModalInternal();
|
|
4079
|
+
return false;
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
startModalSubmission();
|
|
4083
|
+
|
|
4084
|
+
try {
|
|
4085
|
+
if (choice === 'create') {
|
|
4086
|
+
await createSafetyBackupFromModal(modal);
|
|
4087
|
+
}
|
|
4088
|
+
|
|
4089
|
+
if (modal.operation === 'restore') {
|
|
4090
|
+
await restoreBackupById(modal.backupId);
|
|
4091
|
+
} else if (modal.operation === 'sql') {
|
|
4092
|
+
await executeCurrentQuery({ skipSafety: true });
|
|
4093
|
+
} else if (modal.operation === 'import') {
|
|
4094
|
+
await submitImportSql({
|
|
4095
|
+
...(modal.importPayload ?? {}),
|
|
4096
|
+
skipSafety: true,
|
|
4097
|
+
});
|
|
4098
|
+
}
|
|
4099
|
+
|
|
4100
|
+
closeModalInternal();
|
|
4101
|
+
return true;
|
|
4102
|
+
} catch (error) {
|
|
4103
|
+
withModalError(error);
|
|
4104
|
+
return false;
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
|
|
4108
|
+
export async function refreshBackups() {
|
|
4109
|
+
await refreshBackupsState();
|
|
4110
|
+
}
|
|
4111
|
+
|
|
3795
4112
|
export async function openOverviewInFinder() {
|
|
3796
4113
|
try {
|
|
3797
4114
|
const response = await api.openOverviewInFinder();
|
|
@@ -3864,7 +4181,26 @@ export function setEditorTab(tab) {
|
|
|
3864
4181
|
emitChange();
|
|
3865
4182
|
}
|
|
3866
4183
|
|
|
3867
|
-
export async function executeCurrentQuery() {
|
|
4184
|
+
export async function executeCurrentQuery(options = {}) {
|
|
4185
|
+
const riskyOperations = detectRiskySqlOperations(state.editor.sqlText);
|
|
4186
|
+
|
|
4187
|
+
if (!options.skipSafety && riskyOperations.length) {
|
|
4188
|
+
const context = buildRiskySqlBackupContext(riskyOperations);
|
|
4189
|
+
state.modal = {
|
|
4190
|
+
kind: 'backup-safety',
|
|
4191
|
+
operation: 'sql',
|
|
4192
|
+
backupType: context.type,
|
|
4193
|
+
backupName: context.name,
|
|
4194
|
+
notes: state.editor.sqlText.slice(0, 600),
|
|
4195
|
+
description: context.description,
|
|
4196
|
+
sqlText: state.editor.sqlText,
|
|
4197
|
+
error: null,
|
|
4198
|
+
submitting: false,
|
|
4199
|
+
};
|
|
4200
|
+
emitChange();
|
|
4201
|
+
return false;
|
|
4202
|
+
}
|
|
4203
|
+
|
|
3868
4204
|
state.editor.executing = true;
|
|
3869
4205
|
state.editor.lastExecutedSql = state.editor.sqlText;
|
|
3870
4206
|
state.editor.error = null;
|
|
@@ -4140,6 +4476,12 @@ export function setDocumentsSearchQuery(query) {
|
|
|
4140
4476
|
emitChange();
|
|
4141
4477
|
}
|
|
4142
4478
|
|
|
4479
|
+
export function toggleDocumentsPanel() {
|
|
4480
|
+
state.documents.documentsVisible = state.documents.documentsVisible === false;
|
|
4481
|
+
storeBoolean(UI_PREFERENCE_STORAGE_KEYS.documentsVisible, state.documents.documentsVisible);
|
|
4482
|
+
emitChange();
|
|
4483
|
+
}
|
|
4484
|
+
|
|
4143
4485
|
export function setTableDesignerSqlPreviewVisibility(visible) {
|
|
4144
4486
|
const nextValue = typeof visible === 'boolean' ? visible : !Boolean(state.tableDesigner.sqlPreviewVisible);
|
|
4145
4487
|
|
|
@@ -4707,8 +5049,7 @@ export function openDataRowByIdentity(tableName, identity) {
|
|
|
4707
5049
|
export function preserveCurrentDataRowSelectionForReload() {
|
|
4708
5050
|
const tableName = state.dataBrowser.selectedTable ?? state.dataBrowser.table?.name ?? '';
|
|
4709
5051
|
const row = getSelectedDataBrowserRow();
|
|
4710
|
-
const rowIndex =
|
|
4711
|
-
typeof state.dataBrowser.selectedRowIndex === 'number' ? state.dataBrowser.selectedRowIndex : null;
|
|
5052
|
+
const rowIndex = typeof state.dataBrowser.selectedRowIndex === 'number' ? state.dataBrowser.selectedRowIndex : null;
|
|
4712
5053
|
|
|
4713
5054
|
if (!tableName || !row) {
|
|
4714
5055
|
return false;
|
|
@@ -5517,7 +5858,12 @@ export function getQueryMessages(snapshot = state) {
|
|
|
5517
5858
|
|
|
5518
5859
|
return [
|
|
5519
5860
|
...snapshot.editor.result.statements.map(statement => ({
|
|
5520
|
-
tone:
|
|
5861
|
+
tone:
|
|
5862
|
+
statement.kind === 'resultSet' && statement.truncated
|
|
5863
|
+
? 'warning'
|
|
5864
|
+
: statement.kind === 'resultSet'
|
|
5865
|
+
? 'success'
|
|
5866
|
+
: inferStatusTone(statement.keyword),
|
|
5521
5867
|
label: `${statement.keyword} #${statement.index + 1}`,
|
|
5522
5868
|
value:
|
|
5523
5869
|
statement.kind === 'resultSet'
|