sqlite-hub 1.1.1 → 1.2.0
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 +201 -5
- 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/queryResults.js +1 -1
- package/frontend/js/components/rowEditorPanel.js +53 -13
- package/frontend/js/components/sidebar.js +1 -0
- package/frontend/js/components/topNav.js +1 -4
- package/frontend/js/components/workspaceOpenDropdown.js +52 -0
- package/frontend/js/router.js +2 -0
- package/frontend/js/store.js +450 -38
- package/frontend/js/utils/emailPreview.js +28 -0
- package/frontend/js/utils/markdownDocuments.js +17 -1
- package/frontend/js/utils/riskySql.js +165 -0
- package/frontend/js/views/backups.js +204 -0
- package/frontend/js/views/charts.js +1 -1
- package/frontend/js/views/data.js +42 -16
- package/frontend/js/views/documents.js +184 -154
- package/frontend/js/views/settings.js +1 -0
- package/frontend/js/views/structure.js +47 -33
- package/frontend/js/views/tableDesigner.js +23 -0
- package/frontend/styles/components.css +133 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +1 -0
- 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 +70 -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 +101 -0
- package/tests/email-preview.test.js +89 -0
- package/tests/markdown-documents.test.js +24 -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 +53 -0
- package/tests/settings-view.test.js +1 -0
- package/tests/structure-view.test.js +60 -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',
|
|
@@ -82,6 +82,7 @@ let chartsLoadVersion = 0;
|
|
|
82
82
|
let chartsDetailLoadVersion = 0;
|
|
83
83
|
let mediaTaggingPreviewVersion = 0;
|
|
84
84
|
let documentsLoadVersion = 0;
|
|
85
|
+
let lastOpenDocumentId = null;
|
|
85
86
|
|
|
86
87
|
function readStoredDataPageSize(fallback = DEFAULT_DATA_PAGE_SIZE) {
|
|
87
88
|
try {
|
|
@@ -228,6 +229,15 @@ function storeChartsHistoryTab(tab) {
|
|
|
228
229
|
}
|
|
229
230
|
}
|
|
230
231
|
|
|
232
|
+
function readStoredChartsHeightPreset(fallback = 'medium') {
|
|
233
|
+
return normalizeChartsHeightPreset(readStoredString(UI_PREFERENCE_STORAGE_KEYS.chartsHeightPreset, fallback));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function storeChartsHeightPreset(preset) {
|
|
237
|
+
const normalizedPreset = normalizeChartsHeightPreset(preset);
|
|
238
|
+
storeString(UI_PREFERENCE_STORAGE_KEYS.chartsHeightPreset, normalizedPreset);
|
|
239
|
+
}
|
|
240
|
+
|
|
231
241
|
function readStoredQueryHistoryTab(fallback = 'recent') {
|
|
232
242
|
try {
|
|
233
243
|
return normalizeQueryHistoryTab(globalThis.localStorage?.getItem(QUERY_HISTORY_TAB_STORAGE_KEY) ?? fallback);
|
|
@@ -256,6 +266,12 @@ const state = {
|
|
|
256
266
|
backupLoading: false,
|
|
257
267
|
error: null,
|
|
258
268
|
},
|
|
269
|
+
backups: {
|
|
270
|
+
items: [],
|
|
271
|
+
loading: false,
|
|
272
|
+
operationLoading: false,
|
|
273
|
+
error: null,
|
|
274
|
+
},
|
|
259
275
|
settings: {
|
|
260
276
|
data: { ...DEFAULT_SETTINGS },
|
|
261
277
|
section: 'information',
|
|
@@ -344,7 +360,7 @@ const state = {
|
|
|
344
360
|
historyPanelVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsHistoryVisible, true),
|
|
345
361
|
detailPanelVisible: false,
|
|
346
362
|
selectedHistoryId: null,
|
|
347
|
-
chartHeightPreset:
|
|
363
|
+
chartHeightPreset: readStoredChartsHeightPreset(),
|
|
348
364
|
queryVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsQueryVisible, true),
|
|
349
365
|
resultsVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsResultsVisible, true),
|
|
350
366
|
detail: null,
|
|
@@ -362,6 +378,7 @@ const state = {
|
|
|
362
378
|
draftFilename: '',
|
|
363
379
|
draftContent: '',
|
|
364
380
|
dirty: false,
|
|
381
|
+
documentsVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.documentsVisible, true),
|
|
365
382
|
editorVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.documentsEditorVisible, true),
|
|
366
383
|
previewVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.documentsPreviewVisible, true),
|
|
367
384
|
loading: false,
|
|
@@ -573,6 +590,7 @@ function clearQueryHistoryDetailState() {
|
|
|
573
590
|
function requiresActiveDatabase(routeName) {
|
|
574
591
|
return [
|
|
575
592
|
'overview',
|
|
593
|
+
'backups',
|
|
576
594
|
'data',
|
|
577
595
|
'editor',
|
|
578
596
|
'editorResults',
|
|
@@ -611,7 +629,7 @@ function normalizeMediaTaggingRotationDegrees(value) {
|
|
|
611
629
|
return 0;
|
|
612
630
|
}
|
|
613
631
|
|
|
614
|
-
return ((Math.round(numericValue / 90) * 90) % 360 + 360) % 360;
|
|
632
|
+
return (((Math.round(numericValue / 90) * 90) % 360) + 360) % 360;
|
|
615
633
|
}
|
|
616
634
|
|
|
617
635
|
function normalizeDataPageSize(value, fallback = 50) {
|
|
@@ -818,8 +836,10 @@ function areRowIdentitiesEqual(left, right) {
|
|
|
818
836
|
return false;
|
|
819
837
|
}
|
|
820
838
|
|
|
821
|
-
return
|
|
822
|
-
|
|
839
|
+
return (
|
|
840
|
+
JSON.stringify(left.columns ?? []) === JSON.stringify(right.columns ?? []) &&
|
|
841
|
+
JSON.stringify(left.values ?? null) === JSON.stringify(right.values ?? null)
|
|
842
|
+
);
|
|
823
843
|
}
|
|
824
844
|
|
|
825
845
|
function getSelectedDataBrowserRow(snapshot = state) {
|
|
@@ -851,9 +871,7 @@ function clearDataBrowserRowSelectionState() {
|
|
|
851
871
|
|
|
852
872
|
function resolveDataBrowserRowSelection(rowIndex, identity = null) {
|
|
853
873
|
const hasRowIndex =
|
|
854
|
-
rowIndex !== null &&
|
|
855
|
-
rowIndex !== undefined &&
|
|
856
|
-
(typeof rowIndex !== 'string' || rowIndex.trim() !== '');
|
|
874
|
+
rowIndex !== null && rowIndex !== undefined && (typeof rowIndex !== 'string' || rowIndex.trim() !== '');
|
|
857
875
|
const numericIndex = hasRowIndex ? Number(rowIndex) : NaN;
|
|
858
876
|
|
|
859
877
|
if (Number.isInteger(numericIndex) && numericIndex >= 0) {
|
|
@@ -871,7 +889,11 @@ function resolveDataBrowserRowSelection(rowIndex, identity = null) {
|
|
|
871
889
|
const selectedRow = getSelectedDataBrowserRow();
|
|
872
890
|
const selectedIdentity = identity ?? selectedRow?.__identity ?? null;
|
|
873
891
|
|
|
874
|
-
if (
|
|
892
|
+
if (
|
|
893
|
+
!selectedRow?.__identity ||
|
|
894
|
+
!selectedIdentity ||
|
|
895
|
+
!areRowIdentitiesEqual(selectedRow.__identity, selectedIdentity)
|
|
896
|
+
) {
|
|
875
897
|
return {
|
|
876
898
|
row: null,
|
|
877
899
|
rowIndex: null,
|
|
@@ -947,7 +969,7 @@ function resetChartsState() {
|
|
|
947
969
|
state.charts.historyPanelVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsHistoryVisible, true);
|
|
948
970
|
state.charts.detailPanelVisible = false;
|
|
949
971
|
state.charts.selectedHistoryId = null;
|
|
950
|
-
state.charts.chartHeightPreset =
|
|
972
|
+
state.charts.chartHeightPreset = readStoredChartsHeightPreset(state.charts.chartHeightPreset);
|
|
951
973
|
state.charts.queryVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsQueryVisible, true);
|
|
952
974
|
state.charts.resultsVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsResultsVisible, true);
|
|
953
975
|
state.charts.detail = null;
|
|
@@ -982,6 +1004,10 @@ function applyCurrentDocument(document) {
|
|
|
982
1004
|
state.documents.draftContent = document?.content ?? '';
|
|
983
1005
|
state.documents.dirty = false;
|
|
984
1006
|
state.documents.saveError = null;
|
|
1007
|
+
|
|
1008
|
+
if (document?.id) {
|
|
1009
|
+
lastOpenDocumentId = document.id;
|
|
1010
|
+
}
|
|
985
1011
|
}
|
|
986
1012
|
|
|
987
1013
|
function upsertDocumentSummary(document) {
|
|
@@ -1066,7 +1092,9 @@ function syncQueryHistoryItem(updatedItem) {
|
|
|
1066
1092
|
|
|
1067
1093
|
const updatedItemId = String(updatedItem.id);
|
|
1068
1094
|
|
|
1069
|
-
state.editor.history = state.editor.history.map(entry =>
|
|
1095
|
+
state.editor.history = state.editor.history.map(entry =>
|
|
1096
|
+
String(entry.id) === updatedItemId ? updatedItem : entry,
|
|
1097
|
+
);
|
|
1070
1098
|
|
|
1071
1099
|
if (String(state.editor.historyDetail?.id ?? '') === updatedItemId) {
|
|
1072
1100
|
state.editor.historyDetail = updatedItem;
|
|
@@ -1195,7 +1223,10 @@ function setMissingDatabaseState() {
|
|
|
1195
1223
|
state.tableDesigner.selectedTableName = null;
|
|
1196
1224
|
state.tableDesigner.draft = null;
|
|
1197
1225
|
state.tableDesigner.tablesVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerTablesVisible, true);
|
|
1198
|
-
state.tableDesigner.sqlPreviewVisible = readStoredBoolean(
|
|
1226
|
+
state.tableDesigner.sqlPreviewVisible = readStoredBoolean(
|
|
1227
|
+
UI_PREFERENCE_STORAGE_KEYS.tableDesignerSqlPreviewVisible,
|
|
1228
|
+
true,
|
|
1229
|
+
);
|
|
1199
1230
|
state.tableDesigner.pendingImportedDraft = null;
|
|
1200
1231
|
state.tableDesigner.saving = false;
|
|
1201
1232
|
state.tableDesigner.searchQuery = '';
|
|
@@ -1344,6 +1375,30 @@ async function refreshSettingsState() {
|
|
|
1344
1375
|
}
|
|
1345
1376
|
}
|
|
1346
1377
|
|
|
1378
|
+
async function refreshBackupsState() {
|
|
1379
|
+
if (!state.connections.active) {
|
|
1380
|
+
state.backups.items = [];
|
|
1381
|
+
state.backups.error = null;
|
|
1382
|
+
emitChange();
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
state.backups.loading = true;
|
|
1387
|
+
state.backups.error = null;
|
|
1388
|
+
emitChange();
|
|
1389
|
+
|
|
1390
|
+
try {
|
|
1391
|
+
const response = await api.getBackups();
|
|
1392
|
+
state.backups.items = response.data ?? [];
|
|
1393
|
+
state.backups.error = null;
|
|
1394
|
+
} catch (error) {
|
|
1395
|
+
state.backups.error = normalizeError(error);
|
|
1396
|
+
} finally {
|
|
1397
|
+
state.backups.loading = false;
|
|
1398
|
+
emitChange();
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1347
1402
|
export async function createSettingsApiToken(name) {
|
|
1348
1403
|
state.settings.tokenSaving = true;
|
|
1349
1404
|
state.settings.error = null;
|
|
@@ -1387,9 +1442,7 @@ export async function checkSettingsAppVersion() {
|
|
|
1387
1442
|
|
|
1388
1443
|
state.settings.versionCheck = result;
|
|
1389
1444
|
pushToast(
|
|
1390
|
-
result?.updateAvailable
|
|
1391
|
-
? `SQLite Hub v${result.latestVersion} is available.`
|
|
1392
|
-
: 'SQLite Hub is up to date.',
|
|
1445
|
+
result?.updateAvailable ? `SQLite Hub v${result.latestVersion} is available.` : 'SQLite Hub is up to date.',
|
|
1393
1446
|
'success',
|
|
1394
1447
|
);
|
|
1395
1448
|
return result;
|
|
@@ -1671,11 +1724,17 @@ function getRequestedChartsHistoryId(route) {
|
|
|
1671
1724
|
function resolveLoadableChartsHistoryId(route) {
|
|
1672
1725
|
const requestedHistoryId = getRequestedChartsHistoryId(route);
|
|
1673
1726
|
|
|
1674
|
-
if (
|
|
1675
|
-
return null;
|
|
1727
|
+
if (requestedHistoryId) {
|
|
1728
|
+
return state.charts.queries.some(item => item.id === requestedHistoryId) ? requestedHistoryId : null;
|
|
1676
1729
|
}
|
|
1677
1730
|
|
|
1678
|
-
|
|
1731
|
+
const selectedHistoryId = Number(state.charts.selectedHistoryId);
|
|
1732
|
+
|
|
1733
|
+
return Number.isInteger(selectedHistoryId) &&
|
|
1734
|
+
selectedHistoryId > 0 &&
|
|
1735
|
+
state.charts.queries.some(item => item.id === selectedHistoryId)
|
|
1736
|
+
? selectedHistoryId
|
|
1737
|
+
: null;
|
|
1679
1738
|
}
|
|
1680
1739
|
|
|
1681
1740
|
function hasSettledChartsDetail(historyId) {
|
|
@@ -2371,6 +2430,10 @@ async function previewMediaTaggingDraft(options = {}) {
|
|
|
2371
2430
|
function invalidateDatabaseCaches(options = {}) {
|
|
2372
2431
|
const preserveDataBrowserState = options.preserveDataBrowserState === true;
|
|
2373
2432
|
|
|
2433
|
+
if (!preserveDataBrowserState) {
|
|
2434
|
+
lastOpenDocumentId = null;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2374
2437
|
state.overview.data = null;
|
|
2375
2438
|
state.dataBrowser.tables = [];
|
|
2376
2439
|
if (!preserveDataBrowserState) {
|
|
@@ -2391,7 +2454,10 @@ function invalidateDatabaseCaches(options = {}) {
|
|
|
2391
2454
|
state.tableDesigner.selectedTableName = null;
|
|
2392
2455
|
state.tableDesigner.draft = null;
|
|
2393
2456
|
state.tableDesigner.tablesVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerTablesVisible, true);
|
|
2394
|
-
state.tableDesigner.sqlPreviewVisible = readStoredBoolean(
|
|
2457
|
+
state.tableDesigner.sqlPreviewVisible = readStoredBoolean(
|
|
2458
|
+
UI_PREFERENCE_STORAGE_KEYS.tableDesignerSqlPreviewVisible,
|
|
2459
|
+
true,
|
|
2460
|
+
);
|
|
2395
2461
|
state.tableDesigner.pendingImportedDraft = null;
|
|
2396
2462
|
state.tableDesigner.saving = false;
|
|
2397
2463
|
state.tableDesigner.searchQuery = '';
|
|
@@ -2468,6 +2534,9 @@ async function loadRouteData(route, options = {}) {
|
|
|
2468
2534
|
case 'overview':
|
|
2469
2535
|
await loadOverview(version);
|
|
2470
2536
|
return;
|
|
2537
|
+
case 'backups':
|
|
2538
|
+
await refreshBackupsState();
|
|
2539
|
+
return;
|
|
2471
2540
|
case 'data':
|
|
2472
2541
|
await loadData(version, route);
|
|
2473
2542
|
return;
|
|
@@ -2505,7 +2574,7 @@ function pushToast(message, tone = 'muted') {
|
|
|
2505
2574
|
|
|
2506
2575
|
window.setTimeout(() => {
|
|
2507
2576
|
dismissToast(id);
|
|
2508
|
-
},
|
|
2577
|
+
}, 4500);
|
|
2509
2578
|
}
|
|
2510
2579
|
|
|
2511
2580
|
function withModalError(error) {
|
|
@@ -2845,7 +2914,9 @@ function normalizeDocumentInsertionRange(range = null) {
|
|
|
2845
2914
|
const start = Number(range?.start);
|
|
2846
2915
|
const end = Number(range?.end);
|
|
2847
2916
|
const normalizedStart = Number.isInteger(start) ? Math.max(0, Math.min(contentLength, start)) : contentLength;
|
|
2848
|
-
const normalizedEnd = Number.isInteger(end)
|
|
2917
|
+
const normalizedEnd = Number.isInteger(end)
|
|
2918
|
+
? Math.max(normalizedStart, Math.min(contentLength, end))
|
|
2919
|
+
: normalizedStart;
|
|
2849
2920
|
|
|
2850
2921
|
return {
|
|
2851
2922
|
start: normalizedStart,
|
|
@@ -2870,7 +2941,7 @@ function buildDocumentMarkdownInsertion(currentContent, insertion, range = null)
|
|
|
2870
2941
|
return `${before}${prefix}${text}${suffix}${after}`;
|
|
2871
2942
|
}
|
|
2872
2943
|
|
|
2873
|
-
function insertMarkdownIntoCurrentDocumentDraft(markdown, range = null) {
|
|
2944
|
+
export function insertMarkdownIntoCurrentDocumentDraft(markdown, range = null) {
|
|
2874
2945
|
if (!state.documents.selectedId) {
|
|
2875
2946
|
return false;
|
|
2876
2947
|
}
|
|
@@ -2888,6 +2959,63 @@ function insertMarkdownIntoCurrentDocumentDraft(markdown, range = null) {
|
|
|
2888
2959
|
return true;
|
|
2889
2960
|
}
|
|
2890
2961
|
|
|
2962
|
+
export async function insertMarkdownIntoLastOpenDocument(markdown) {
|
|
2963
|
+
if (state.route.name === 'documents' && state.documents.selectedId) {
|
|
2964
|
+
const inserted = insertMarkdownIntoCurrentDocumentDraft(markdown);
|
|
2965
|
+
|
|
2966
|
+
return {
|
|
2967
|
+
documentId: state.documents.selectedId,
|
|
2968
|
+
inserted,
|
|
2969
|
+
saved: false,
|
|
2970
|
+
};
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
const documentId = lastOpenDocumentId ?? state.documents.selectedId;
|
|
2974
|
+
|
|
2975
|
+
if (!documentId) {
|
|
2976
|
+
return {
|
|
2977
|
+
documentId: null,
|
|
2978
|
+
inserted: false,
|
|
2979
|
+
saved: false,
|
|
2980
|
+
};
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
const response = await api.getDocument(documentId);
|
|
2984
|
+
const document = response.data ?? null;
|
|
2985
|
+
|
|
2986
|
+
if (!document?.id) {
|
|
2987
|
+
return {
|
|
2988
|
+
documentId: null,
|
|
2989
|
+
inserted: false,
|
|
2990
|
+
saved: false,
|
|
2991
|
+
};
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
const nextContent = buildDocumentMarkdownInsertion(document.content, markdown);
|
|
2995
|
+
|
|
2996
|
+
if (nextContent === document.content) {
|
|
2997
|
+
return {
|
|
2998
|
+
documentId: document.id,
|
|
2999
|
+
inserted: false,
|
|
3000
|
+
saved: false,
|
|
3001
|
+
};
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
const updateResponse = await api.updateDocument(document.id, {
|
|
3005
|
+
content: nextContent,
|
|
3006
|
+
});
|
|
3007
|
+
const updatedDocument = updateResponse.data ?? document;
|
|
3008
|
+
|
|
3009
|
+
lastOpenDocumentId = updatedDocument.id;
|
|
3010
|
+
upsertDocumentSummary(updatedDocument);
|
|
3011
|
+
|
|
3012
|
+
return {
|
|
3013
|
+
documentId: updatedDocument.id,
|
|
3014
|
+
inserted: true,
|
|
3015
|
+
saved: true,
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
|
|
2891
3019
|
function getDocumentInsertQueryTitle(query) {
|
|
2892
3020
|
return query?.displayTitle || query?.title || query?.previewSql || query?.rawSql || 'Saved query';
|
|
2893
3021
|
}
|
|
@@ -3154,7 +3282,8 @@ export async function deleteCurrentDocument(options = {}) {
|
|
|
3154
3282
|
try {
|
|
3155
3283
|
await api.deleteDocument(documentId);
|
|
3156
3284
|
state.documents.items = state.documents.items.filter(item => item.id !== documentId);
|
|
3157
|
-
const nextDocument =
|
|
3285
|
+
const nextDocument =
|
|
3286
|
+
state.documents.items[Math.max(0, Math.min(deletedIndex, state.documents.items.length - 1))] ?? null;
|
|
3158
3287
|
if (isSelectedDocument) {
|
|
3159
3288
|
applyCurrentDocument(null);
|
|
3160
3289
|
}
|
|
@@ -3244,9 +3373,7 @@ export function openDeleteDataRowModal(rowIndex) {
|
|
|
3244
3373
|
const tableName = state.dataBrowser.selectedTable;
|
|
3245
3374
|
const numericIndex = Number(rowIndex);
|
|
3246
3375
|
const hasNumericIndex = Number.isInteger(numericIndex) && numericIndex >= 0;
|
|
3247
|
-
const row = hasNumericIndex
|
|
3248
|
-
? state.dataBrowser.table?.rows?.[numericIndex] ?? null
|
|
3249
|
-
: getSelectedDataBrowserRow();
|
|
3376
|
+
const row = hasNumericIndex ? (state.dataBrowser.table?.rows?.[numericIndex] ?? null) : getSelectedDataBrowserRow();
|
|
3250
3377
|
const rowPreview = buildDeleteRowPreview(
|
|
3251
3378
|
(state.dataBrowser.table?.columnMeta ?? [])
|
|
3252
3379
|
.filter(column => column.visible)
|
|
@@ -3541,6 +3668,7 @@ export function setChartsHeightPreset(preset) {
|
|
|
3541
3668
|
}
|
|
3542
3669
|
|
|
3543
3670
|
state.charts.chartHeightPreset = nextPreset;
|
|
3671
|
+
storeChartsHeightPreset(nextPreset);
|
|
3544
3672
|
emitChange();
|
|
3545
3673
|
}
|
|
3546
3674
|
|
|
@@ -3691,10 +3819,36 @@ export async function chooseCreateDatabasePath() {
|
|
|
3691
3819
|
}
|
|
3692
3820
|
|
|
3693
3821
|
export async function submitImportSql(payload) {
|
|
3822
|
+
if (!payload?.skipSafety && !payload?.createNew && !payload?.targetConnectionId && !payload?.targetPath) {
|
|
3823
|
+
try {
|
|
3824
|
+
const preview = await api.previewImportSql(payload);
|
|
3825
|
+
|
|
3826
|
+
if (preview.data?.requiresSafetyBackup) {
|
|
3827
|
+
state.modal = {
|
|
3828
|
+
kind: 'backup-safety',
|
|
3829
|
+
operation: 'import',
|
|
3830
|
+
importPayload: payload,
|
|
3831
|
+
backupType: 'pre_import',
|
|
3832
|
+
backupName: 'Before import',
|
|
3833
|
+
notes: `SQL import, ${preview.data.statementCount ?? 0} statements, ${preview.data.sizeBytes ?? 0} bytes`,
|
|
3834
|
+
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.',
|
|
3835
|
+
error: null,
|
|
3836
|
+
submitting: false,
|
|
3837
|
+
};
|
|
3838
|
+
emitChange();
|
|
3839
|
+
return null;
|
|
3840
|
+
}
|
|
3841
|
+
} catch (error) {
|
|
3842
|
+
withModalError(error);
|
|
3843
|
+
return null;
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3694
3847
|
startModalSubmission();
|
|
3695
3848
|
|
|
3696
3849
|
try {
|
|
3697
|
-
const
|
|
3850
|
+
const { skipSafety, ...requestPayload } = payload ?? {};
|
|
3851
|
+
const response = await api.importSql(requestPayload);
|
|
3698
3852
|
closeModalInternal();
|
|
3699
3853
|
pushToast(response.message || 'SQL dump imported.', 'success');
|
|
3700
3854
|
await refreshConnectionsState();
|
|
@@ -3770,28 +3924,257 @@ export async function removeConnection(id) {
|
|
|
3770
3924
|
}
|
|
3771
3925
|
|
|
3772
3926
|
export async function createActiveConnectionBackup() {
|
|
3927
|
+
openCreateBackupModal();
|
|
3928
|
+
return null;
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3931
|
+
export function openCreateBackupModal(defaults = {}) {
|
|
3773
3932
|
if (!state.connections.active) {
|
|
3774
3933
|
pushToast('No active SQLite database selected for backup.', 'alert');
|
|
3934
|
+
return;
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
state.modal = {
|
|
3938
|
+
kind: 'create-backup',
|
|
3939
|
+
name: defaults.name ?? '',
|
|
3940
|
+
notes: defaults.notes ?? '',
|
|
3941
|
+
backupType: defaults.type ?? 'manual',
|
|
3942
|
+
error: null,
|
|
3943
|
+
submitting: false,
|
|
3944
|
+
};
|
|
3945
|
+
emitChange();
|
|
3946
|
+
}
|
|
3947
|
+
|
|
3948
|
+
export async function submitCreateBackupConfirmation({ name = '', notes = '', type = 'manual' } = {}) {
|
|
3949
|
+
if (state.modal?.kind !== 'create-backup') {
|
|
3775
3950
|
return null;
|
|
3776
3951
|
}
|
|
3777
3952
|
|
|
3953
|
+
startModalSubmission();
|
|
3778
3954
|
state.connections.backupLoading = true;
|
|
3779
|
-
|
|
3955
|
+
state.backups.operationLoading = true;
|
|
3780
3956
|
|
|
3781
3957
|
try {
|
|
3782
|
-
const response = await api.
|
|
3783
|
-
|
|
3958
|
+
const response = await api.createBackup({ name, notes, type });
|
|
3959
|
+
state.backups.items = [
|
|
3960
|
+
response.data,
|
|
3961
|
+
...state.backups.items.filter(item => item.id !== response.data.id),
|
|
3962
|
+
];
|
|
3963
|
+
closeModalInternal();
|
|
3784
3964
|
pushToast(response.message || 'Backup created.', 'success');
|
|
3965
|
+
if (state.route.name === 'backups') {
|
|
3966
|
+
await refreshBackupsState();
|
|
3967
|
+
}
|
|
3785
3968
|
return response.data;
|
|
3786
3969
|
} catch (error) {
|
|
3787
|
-
|
|
3970
|
+
withModalError(error);
|
|
3788
3971
|
return null;
|
|
3789
3972
|
} finally {
|
|
3790
3973
|
state.connections.backupLoading = false;
|
|
3974
|
+
state.backups.operationLoading = false;
|
|
3791
3975
|
emitChange();
|
|
3792
3976
|
}
|
|
3793
3977
|
}
|
|
3794
3978
|
|
|
3979
|
+
export function openEditBackupNotesModal(backupId) {
|
|
3980
|
+
const backup = state.backups.items.find(item => String(item.id) === String(backupId));
|
|
3981
|
+
|
|
3982
|
+
if (!backup) {
|
|
3983
|
+
pushToast('The selected backup could not be loaded.', 'alert');
|
|
3984
|
+
return;
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3987
|
+
state.modal = {
|
|
3988
|
+
kind: 'edit-backup-notes',
|
|
3989
|
+
backupId: backup.id,
|
|
3990
|
+
backupName: backup.name,
|
|
3991
|
+
notes: backup.notes ?? '',
|
|
3992
|
+
error: null,
|
|
3993
|
+
submitting: false,
|
|
3994
|
+
};
|
|
3995
|
+
emitChange();
|
|
3996
|
+
}
|
|
3997
|
+
|
|
3998
|
+
export async function submitEditBackupNotesConfirmation(notes = '') {
|
|
3999
|
+
if (state.modal?.kind !== 'edit-backup-notes') {
|
|
4000
|
+
return null;
|
|
4001
|
+
}
|
|
4002
|
+
|
|
4003
|
+
const backupId = state.modal.backupId;
|
|
4004
|
+
startModalSubmission();
|
|
4005
|
+
state.backups.operationLoading = true;
|
|
4006
|
+
|
|
4007
|
+
try {
|
|
4008
|
+
const response = await api.updateBackup(backupId, { notes });
|
|
4009
|
+
state.backups.items = state.backups.items.map(item =>
|
|
4010
|
+
String(item.id) === String(response.data.id) ? response.data : item
|
|
4011
|
+
);
|
|
4012
|
+
closeModalInternal();
|
|
4013
|
+
pushToast(response.message || 'Backup notes updated.', 'success');
|
|
4014
|
+
return response.data;
|
|
4015
|
+
} catch (error) {
|
|
4016
|
+
withModalError(error);
|
|
4017
|
+
return null;
|
|
4018
|
+
} finally {
|
|
4019
|
+
state.backups.operationLoading = false;
|
|
4020
|
+
emitChange();
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
|
|
4024
|
+
export function openDeleteBackupModal(backupId) {
|
|
4025
|
+
const backup = state.backups.items.find(item => String(item.id) === String(backupId));
|
|
4026
|
+
|
|
4027
|
+
if (!backup) {
|
|
4028
|
+
pushToast('The selected backup could not be loaded.', 'alert');
|
|
4029
|
+
return;
|
|
4030
|
+
}
|
|
4031
|
+
|
|
4032
|
+
state.modal = {
|
|
4033
|
+
kind: 'delete-backup',
|
|
4034
|
+
backupId: backup.id,
|
|
4035
|
+
backupName: backup.name,
|
|
4036
|
+
fileName: backup.fileName,
|
|
4037
|
+
error: null,
|
|
4038
|
+
submitting: false,
|
|
4039
|
+
};
|
|
4040
|
+
emitChange();
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4043
|
+
export async function submitDeleteBackupConfirmation() {
|
|
4044
|
+
if (state.modal?.kind !== 'delete-backup') {
|
|
4045
|
+
return false;
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
const backupId = state.modal.backupId;
|
|
4049
|
+
startModalSubmission();
|
|
4050
|
+
state.backups.operationLoading = true;
|
|
4051
|
+
|
|
4052
|
+
try {
|
|
4053
|
+
const response = await api.deleteBackup(backupId);
|
|
4054
|
+
state.backups.items = state.backups.items.filter(item => String(item.id) !== String(backupId));
|
|
4055
|
+
closeModalInternal();
|
|
4056
|
+
pushToast(response.message || 'Backup deleted.', 'muted');
|
|
4057
|
+
return true;
|
|
4058
|
+
} catch (error) {
|
|
4059
|
+
withModalError(error);
|
|
4060
|
+
return false;
|
|
4061
|
+
} finally {
|
|
4062
|
+
state.backups.operationLoading = false;
|
|
4063
|
+
emitChange();
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
export async function downloadBackup(backupId) {
|
|
4068
|
+
state.backups.operationLoading = true;
|
|
4069
|
+
emitChange();
|
|
4070
|
+
|
|
4071
|
+
try {
|
|
4072
|
+
await api.downloadBackup(backupId);
|
|
4073
|
+
pushToast('Backup download started.', 'success');
|
|
4074
|
+
return true;
|
|
4075
|
+
} catch (error) {
|
|
4076
|
+
state.backups.error = normalizeError(error);
|
|
4077
|
+
pushToast(state.backups.error.message || 'Backup could not be downloaded.', 'alert');
|
|
4078
|
+
return false;
|
|
4079
|
+
} finally {
|
|
4080
|
+
state.backups.operationLoading = false;
|
|
4081
|
+
emitChange();
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
|
|
4085
|
+
export function openRestoreBackupModal(backupId) {
|
|
4086
|
+
const backup = state.backups.items.find(item => String(item.id) === String(backupId));
|
|
4087
|
+
|
|
4088
|
+
if (!backup) {
|
|
4089
|
+
pushToast('The selected backup could not be loaded.', 'alert');
|
|
4090
|
+
return;
|
|
4091
|
+
}
|
|
4092
|
+
|
|
4093
|
+
state.modal = {
|
|
4094
|
+
kind: 'backup-safety',
|
|
4095
|
+
operation: 'restore',
|
|
4096
|
+
backupId: backup.id,
|
|
4097
|
+
backupName: backup.name,
|
|
4098
|
+
backupType: 'pre_restore',
|
|
4099
|
+
backupNameBeforeOperation: `Before restore of ${backup.name}`,
|
|
4100
|
+
description: 'Restoring replaces the current database file with this backup.',
|
|
4101
|
+
error: null,
|
|
4102
|
+
submitting: false,
|
|
4103
|
+
};
|
|
4104
|
+
emitChange();
|
|
4105
|
+
}
|
|
4106
|
+
|
|
4107
|
+
async function restoreBackupById(backupId) {
|
|
4108
|
+
state.backups.operationLoading = true;
|
|
4109
|
+
|
|
4110
|
+
try {
|
|
4111
|
+
const response = await api.restoreBackup(backupId);
|
|
4112
|
+
pushToast(response.message || 'Backup restored.', 'success');
|
|
4113
|
+
await Promise.all([refreshConnectionsState(), refreshBackupsState()]);
|
|
4114
|
+
invalidateDatabaseCaches();
|
|
4115
|
+
return response.data;
|
|
4116
|
+
} finally {
|
|
4117
|
+
state.backups.operationLoading = false;
|
|
4118
|
+
emitChange();
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
|
|
4122
|
+
async function createSafetyBackupFromModal(modal) {
|
|
4123
|
+
const response = await api.createBackup({
|
|
4124
|
+
name: modal.backupNameBeforeOperation ?? modal.backupName,
|
|
4125
|
+
notes: modal.notes,
|
|
4126
|
+
type: modal.backupType,
|
|
4127
|
+
});
|
|
4128
|
+
|
|
4129
|
+
if (state.route.name === 'backups') {
|
|
4130
|
+
await refreshBackupsState();
|
|
4131
|
+
}
|
|
4132
|
+
|
|
4133
|
+
return response.data;
|
|
4134
|
+
}
|
|
4135
|
+
|
|
4136
|
+
export async function submitBackupSafetyChoice(choice) {
|
|
4137
|
+
const modal = state.modal;
|
|
4138
|
+
|
|
4139
|
+
if (modal?.kind !== 'backup-safety') {
|
|
4140
|
+
return false;
|
|
4141
|
+
}
|
|
4142
|
+
|
|
4143
|
+
if (choice === 'cancel') {
|
|
4144
|
+
closeModalInternal();
|
|
4145
|
+
return false;
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4148
|
+
startModalSubmission();
|
|
4149
|
+
|
|
4150
|
+
try {
|
|
4151
|
+
if (choice === 'create') {
|
|
4152
|
+
await createSafetyBackupFromModal(modal);
|
|
4153
|
+
}
|
|
4154
|
+
|
|
4155
|
+
if (modal.operation === 'restore') {
|
|
4156
|
+
await restoreBackupById(modal.backupId);
|
|
4157
|
+
} else if (modal.operation === 'sql') {
|
|
4158
|
+
await executeCurrentQuery({ skipSafety: true });
|
|
4159
|
+
} else if (modal.operation === 'import') {
|
|
4160
|
+
await submitImportSql({
|
|
4161
|
+
...(modal.importPayload ?? {}),
|
|
4162
|
+
skipSafety: true,
|
|
4163
|
+
});
|
|
4164
|
+
}
|
|
4165
|
+
|
|
4166
|
+
closeModalInternal();
|
|
4167
|
+
return true;
|
|
4168
|
+
} catch (error) {
|
|
4169
|
+
withModalError(error);
|
|
4170
|
+
return false;
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
|
|
4174
|
+
export async function refreshBackups() {
|
|
4175
|
+
await refreshBackupsState();
|
|
4176
|
+
}
|
|
4177
|
+
|
|
3795
4178
|
export async function openOverviewInFinder() {
|
|
3796
4179
|
try {
|
|
3797
4180
|
const response = await api.openOverviewInFinder();
|
|
@@ -3864,7 +4247,26 @@ export function setEditorTab(tab) {
|
|
|
3864
4247
|
emitChange();
|
|
3865
4248
|
}
|
|
3866
4249
|
|
|
3867
|
-
export async function executeCurrentQuery() {
|
|
4250
|
+
export async function executeCurrentQuery(options = {}) {
|
|
4251
|
+
const riskyOperations = detectRiskySqlOperations(state.editor.sqlText);
|
|
4252
|
+
|
|
4253
|
+
if (!options.skipSafety && riskyOperations.length) {
|
|
4254
|
+
const context = buildRiskySqlBackupContext(riskyOperations);
|
|
4255
|
+
state.modal = {
|
|
4256
|
+
kind: 'backup-safety',
|
|
4257
|
+
operation: 'sql',
|
|
4258
|
+
backupType: context.type,
|
|
4259
|
+
backupName: context.name,
|
|
4260
|
+
notes: state.editor.sqlText.slice(0, 600),
|
|
4261
|
+
description: context.description,
|
|
4262
|
+
sqlText: state.editor.sqlText,
|
|
4263
|
+
error: null,
|
|
4264
|
+
submitting: false,
|
|
4265
|
+
};
|
|
4266
|
+
emitChange();
|
|
4267
|
+
return false;
|
|
4268
|
+
}
|
|
4269
|
+
|
|
3868
4270
|
state.editor.executing = true;
|
|
3869
4271
|
state.editor.lastExecutedSql = state.editor.sqlText;
|
|
3870
4272
|
state.editor.error = null;
|
|
@@ -4140,6 +4542,12 @@ export function setDocumentsSearchQuery(query) {
|
|
|
4140
4542
|
emitChange();
|
|
4141
4543
|
}
|
|
4142
4544
|
|
|
4545
|
+
export function toggleDocumentsPanel() {
|
|
4546
|
+
state.documents.documentsVisible = state.documents.documentsVisible === false;
|
|
4547
|
+
storeBoolean(UI_PREFERENCE_STORAGE_KEYS.documentsVisible, state.documents.documentsVisible);
|
|
4548
|
+
emitChange();
|
|
4549
|
+
}
|
|
4550
|
+
|
|
4143
4551
|
export function setTableDesignerSqlPreviewVisibility(visible) {
|
|
4144
4552
|
const nextValue = typeof visible === 'boolean' ? visible : !Boolean(state.tableDesigner.sqlPreviewVisible);
|
|
4145
4553
|
|
|
@@ -4707,8 +5115,7 @@ export function openDataRowByIdentity(tableName, identity) {
|
|
|
4707
5115
|
export function preserveCurrentDataRowSelectionForReload() {
|
|
4708
5116
|
const tableName = state.dataBrowser.selectedTable ?? state.dataBrowser.table?.name ?? '';
|
|
4709
5117
|
const row = getSelectedDataBrowserRow();
|
|
4710
|
-
const rowIndex =
|
|
4711
|
-
typeof state.dataBrowser.selectedRowIndex === 'number' ? state.dataBrowser.selectedRowIndex : null;
|
|
5118
|
+
const rowIndex = typeof state.dataBrowser.selectedRowIndex === 'number' ? state.dataBrowser.selectedRowIndex : null;
|
|
4712
5119
|
|
|
4713
5120
|
if (!tableName || !row) {
|
|
4714
5121
|
return false;
|
|
@@ -5517,7 +5924,12 @@ export function getQueryMessages(snapshot = state) {
|
|
|
5517
5924
|
|
|
5518
5925
|
return [
|
|
5519
5926
|
...snapshot.editor.result.statements.map(statement => ({
|
|
5520
|
-
tone:
|
|
5927
|
+
tone:
|
|
5928
|
+
statement.kind === 'resultSet' && statement.truncated
|
|
5929
|
+
? 'warning'
|
|
5930
|
+
: statement.kind === 'resultSet'
|
|
5931
|
+
? 'success'
|
|
5932
|
+
: inferStatusTone(statement.keyword),
|
|
5521
5933
|
label: `${statement.keyword} #${statement.index + 1}`,
|
|
5522
5934
|
value:
|
|
5523
5935
|
statement.kind === 'resultSet'
|