mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-30 16:21:39 +00:00
perf: full-stack performance and security hardening
Frontend performance: - Debounce DataGrid search (150ms) and replace O(n) row wrapping with index array - Replace QueryStore deep watcher with targeted field-level persistence snapshot - Extract shared nodeConfig computed in TreeItem (eliminates 15x redundant getConfig calls) - Clear tab result data on close to free memory - Add LRU eviction for completion caches (50 entry cap) - Replace savedSql version string join with numeric counter Memory leak fixes: - Store Tauri event unlisten handles and clean up on unmount - Clean up DataGrid search timer on unmount Backend security & resource management: - Add 50MB request body size limit - Reject file uploads >100MB and SQL files >200MB - Clean up SSE channels after transfer/import completes - Recover from poisoned mutex instead of panicking - Delete temporary upload files after preview - Validate SQL file paths to prevent directory traversal
This commit is contained in:
+2
-1
@@ -128,7 +128,7 @@ const { getDatabaseOptions } = useDatabaseOptions();
|
||||
const { openLineageTarget, openDatabaseSearchTarget, onStructureEditorSaved, openTableTarget } =
|
||||
useNavigationTargets(dialogs);
|
||||
const { onExecuteSql, onReloadData, onPaginate, onSort } = useDataGridActions(activeTab);
|
||||
const { setupTauriListeners } = useTauriEvents({ openTableTarget });
|
||||
const { setupTauriListeners, cleanupTauriListeners } = useTauriEvents({ openTableTarget });
|
||||
|
||||
const appVersion = ref("");
|
||||
const isClassicLayout = computed(() => settingsStore.editorSettings.appLayout === "classic");
|
||||
@@ -494,6 +494,7 @@ onMounted(async () => {
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanupTauriListeners();
|
||||
window.removeEventListener("keydown", handleKeydown, true);
|
||||
document.removeEventListener("contextmenu", handleContextMenu);
|
||||
});
|
||||
|
||||
@@ -219,6 +219,9 @@ const sortCol = ref<string | null>(null);
|
||||
const sortColIndex = ref<number | null>(null);
|
||||
const sortDir = ref<"asc" | "desc">("asc");
|
||||
const searchText = ref("");
|
||||
const deferredClientSearchText = ref("");
|
||||
let _searchTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
const searchSuggestions = ref<string[]>([]);
|
||||
const suggestionIndex = ref(-1);
|
||||
const searchInputRef = ref<HTMLInputElement>();
|
||||
@@ -279,9 +282,18 @@ function rowMatchesLocalColumnFilters(data: CellValue[]): boolean {
|
||||
}
|
||||
|
||||
const localFilteredRows = computed(() => {
|
||||
const rows = props.result.rows.map((row, sourceIndex) => ({ row, sourceIndex }));
|
||||
if (!hasLocalColumnFilters.value) return rows;
|
||||
return rows.filter(({ row, sourceIndex }) => rowMatchesLocalColumnFilters(rowDataWithChanges(row, sourceIndex)));
|
||||
const rows = props.result.rows;
|
||||
const indices: number[] = [];
|
||||
if (!hasLocalColumnFilters.value) {
|
||||
for (let i = 0; i < rows.length; i++) indices.push(i);
|
||||
return indices;
|
||||
}
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
if (rowMatchesLocalColumnFilters(rowDataWithChanges(rows[i], i))) {
|
||||
indices.push(i);
|
||||
}
|
||||
}
|
||||
return indices;
|
||||
});
|
||||
|
||||
function buildLocalFilterOptions(columnIndex: number) {
|
||||
@@ -838,6 +850,17 @@ const isFullPage = computed(() => props.result.rows.length >= pageSize.value);
|
||||
const isResultsContext = computed(() => props.context === "results");
|
||||
const canUseWhereSearch = computed(() => !!props.tableMeta && !!props.onExecuteSql && !isResultsContext.value);
|
||||
const clientSearchText = computed(() => (searchText.value.trim() ? searchText.value : ""));
|
||||
watch(clientSearchText, (value) => {
|
||||
clearTimeout(_searchTimer);
|
||||
const q = value.trim().toLowerCase();
|
||||
if (!q) {
|
||||
deferredClientSearchText.value = "";
|
||||
return;
|
||||
}
|
||||
_searchTimer = setTimeout(() => {
|
||||
deferredClientSearchText.value = q;
|
||||
}, 150);
|
||||
});
|
||||
|
||||
function currentWhereInput(): string | undefined {
|
||||
return whereFilterInput.value.trim() || undefined;
|
||||
@@ -936,15 +959,16 @@ function onToolbarRollback() {
|
||||
}
|
||||
|
||||
const sortedRows = computed(() => {
|
||||
let rows = localFilteredRows.value;
|
||||
if (clientSearchText.value) {
|
||||
const q = clientSearchText.value.toLowerCase();
|
||||
rows = rows.filter(({ row, sourceIndex }) => {
|
||||
const data = rowDataWithChanges(row, sourceIndex);
|
||||
let indices = localFilteredRows.value;
|
||||
const q = deferredClientSearchText.value;
|
||||
if (q) {
|
||||
const rows = props.result.rows;
|
||||
indices = indices.filter((sourceIndex) => {
|
||||
const data = rowDataWithChanges(rows[sourceIndex], sourceIndex);
|
||||
return data.some((cell) => cell !== null && String(cell).toLowerCase().includes(q));
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
return indices;
|
||||
});
|
||||
|
||||
function rowDataWithChanges(row: CellValue[], sourceIndex: number): CellValue[] {
|
||||
@@ -965,7 +989,9 @@ interface RowItem {
|
||||
|
||||
const displayItems = computed<RowItem[]>(() => {
|
||||
const cols = props.result.columns;
|
||||
const items: RowItem[] = sortedRows.value.map(({ row, sourceIndex }) => {
|
||||
const rows = props.result.rows;
|
||||
const items: RowItem[] = sortedRows.value.map((sourceIndex) => {
|
||||
const row = rows[sourceIndex];
|
||||
const dirty = dirtyRows.value.get(sourceIndex);
|
||||
const data = rowDataWithChanges(row, sourceIndex);
|
||||
const isDirtyCol = row.map((_, colIdx) => dirty?.has(colIdx) ?? false);
|
||||
@@ -989,7 +1015,7 @@ const displayItems = computed<RowItem[]>(() => {
|
||||
});
|
||||
const hasVisibleRows = computed(() => displayItems.value.length > 0);
|
||||
const hasActiveFilter = computed(
|
||||
() => !!clientSearchText.value || rowStatusFilter.value !== "all" || hasLocalColumnFilters.value,
|
||||
() => !!deferredClientSearchText.value || rowStatusFilter.value !== "all" || hasLocalColumnFilters.value,
|
||||
);
|
||||
const totalFilterableRowCount = computed(() => props.result.rows.length + newRows.value.length);
|
||||
const emptyTitle = computed(() => (hasActiveFilter.value ? t("grid.noFilteredRows") : t("grid.noRows")));
|
||||
@@ -2130,6 +2156,7 @@ onUnmounted(() => {
|
||||
if (cancelScrollRestoreFrame) cancelAnimationFrame(cancelScrollRestoreFrame);
|
||||
onDdlResizeEnd();
|
||||
finishCellSelection();
|
||||
clearTimeout(_searchTimer);
|
||||
});
|
||||
|
||||
const SQL_KEYWORDS =
|
||||
|
||||
@@ -1112,52 +1112,51 @@ function openFieldLineage() {
|
||||
}
|
||||
|
||||
const canExpand = !leafTypes.has(props.node.type);
|
||||
const nodeConfig = computed(() =>
|
||||
props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined,
|
||||
);
|
||||
const canPin = computed(() => pinnableTypes.has(props.node.type));
|
||||
const canOpenSqlFileExecution = computed(() => {
|
||||
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
|
||||
return !!config && !SQL_FILE_UNSUPPORTED_TYPES.has(config.db_type);
|
||||
return !!nodeConfig.value && !SQL_FILE_UNSUPPORTED_TYPES.has(nodeConfig.value.db_type);
|
||||
});
|
||||
const canOpenDiagram = computed(() => {
|
||||
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
|
||||
return !!props.node.database && !!config && DIAGRAM_SUPPORTED_TYPES.has(config.db_type);
|
||||
return !!props.node.database && !!nodeConfig.value && DIAGRAM_SUPPORTED_TYPES.has(nodeConfig.value.db_type);
|
||||
});
|
||||
const canOpenDatabaseSearch = computed(() => {
|
||||
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
|
||||
return !!props.node.database && !!config && DATABASE_SEARCH_SUPPORTED_TYPES.has(config.db_type);
|
||||
return !!props.node.database && !!nodeConfig.value && DATABASE_SEARCH_SUPPORTED_TYPES.has(nodeConfig.value.db_type);
|
||||
});
|
||||
const canOpenObjectBrowser = computed(() => {
|
||||
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
|
||||
return (
|
||||
(props.node.type === "database" || props.node.type === "schema" || props.node.type === "object-browser") &&
|
||||
!!config &&
|
||||
config.db_type !== "redis" &&
|
||||
config.db_type !== "mongodb" &&
|
||||
config.db_type !== "elasticsearch"
|
||||
!!nodeConfig.value &&
|
||||
nodeConfig.value.db_type !== "redis" &&
|
||||
nodeConfig.value.db_type !== "mongodb" &&
|
||||
nodeConfig.value.db_type !== "elasticsearch"
|
||||
);
|
||||
});
|
||||
const canOpenTableImport = computed(() => {
|
||||
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
|
||||
return (
|
||||
props.node.type === "table" && !!props.node.database && !!config && TABLE_IMPORT_SUPPORTED_TYPES.has(config.db_type)
|
||||
);
|
||||
});
|
||||
const canOpenStructureEditor = computed(() => {
|
||||
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
|
||||
return (
|
||||
props.node.type === "table" &&
|
||||
!!props.node.database &&
|
||||
!!config &&
|
||||
TABLE_STRUCTURE_SUPPORTED_TYPES.has(config.db_type)
|
||||
!!nodeConfig.value &&
|
||||
TABLE_IMPORT_SUPPORTED_TYPES.has(nodeConfig.value.db_type)
|
||||
);
|
||||
});
|
||||
const canOpenStructureEditor = computed(() => {
|
||||
return (
|
||||
props.node.type === "table" &&
|
||||
!!props.node.database &&
|
||||
!!nodeConfig.value &&
|
||||
TABLE_STRUCTURE_SUPPORTED_TYPES.has(nodeConfig.value.db_type)
|
||||
);
|
||||
});
|
||||
const canOpenFieldLineage = computed(() => {
|
||||
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
|
||||
return (
|
||||
props.node.type === "column" &&
|
||||
!!props.node.database &&
|
||||
!!props.node.tableName &&
|
||||
!!config &&
|
||||
FIELD_LINEAGE_SUPPORTED_TYPES.has(config.db_type)
|
||||
!!nodeConfig.value &&
|
||||
FIELD_LINEAGE_SUPPORTED_TYPES.has(nodeConfig.value.db_type)
|
||||
);
|
||||
});
|
||||
const isPinned = computed(() => props.node.pinned || connectionStore.isTreeNodePinned(props.node.id));
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { NavigationTarget } from "@/composables/useNavigationTargets";
|
||||
export function useTauriEvents(deps: { openTableTarget: (target: NavigationTarget) => Promise<void> }) {
|
||||
const connectionStore = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
const unlistenHandles: Array<() => void> = [];
|
||||
|
||||
function setupTauriListeners() {
|
||||
import("@tauri-apps/api/event")
|
||||
@@ -12,53 +13,67 @@ export function useTauriEvents(deps: { openTableTarget: (target: NavigationTarge
|
||||
listen<{ connection_id: string; database: string; schema?: string; table: string }>(
|
||||
"mcp-open-table",
|
||||
async (event) => {
|
||||
const { connection_id, database, schema, table } = event.payload;
|
||||
try {
|
||||
const { connection_id, database, schema, table } = event.payload;
|
||||
if (!connectionStore.connections.length) await connectionStore.initFromDisk();
|
||||
const config = connectionStore.getConfig(connection_id);
|
||||
if (!config) return;
|
||||
connectionStore.activeConnectionId = connection_id;
|
||||
await connectionStore.ensureConnected(connection_id);
|
||||
if (config.db_type === "redis") {
|
||||
queryStore.createTab(connection_id, database || "0", `db${database || "0"}`, "redis");
|
||||
} else if (config.db_type === "mongodb") {
|
||||
queryStore.createTab(connection_id, database, table, "mongo");
|
||||
} else {
|
||||
deps.openTableTarget({ connectionId: connection_id, database, schema, tableName: table });
|
||||
}
|
||||
import("@tauri-apps/api/window").then(({ getCurrentWindow }) =>
|
||||
getCurrentWindow()
|
||||
.setFocus()
|
||||
.catch(() => {}),
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("[DBX] mcp-open-table error:", e);
|
||||
}
|
||||
},
|
||||
).then((unlisten) => unlistenHandles.push(unlisten));
|
||||
|
||||
listen("mcp-reload-connections", async () => {
|
||||
try {
|
||||
await connectionStore.initFromDisk();
|
||||
} catch (e) {
|
||||
console.error("[DBX] mcp-reload-connections error:", e);
|
||||
}
|
||||
}).then((unlisten) => unlistenHandles.push(unlisten));
|
||||
|
||||
listen<{ connection_id: string; database: string; sql: string }>("mcp-execute-query", async (event) => {
|
||||
try {
|
||||
const { connection_id, database, sql } = event.payload;
|
||||
if (!connectionStore.connections.length) await connectionStore.initFromDisk();
|
||||
const config = connectionStore.getConfig(connection_id);
|
||||
if (!config) return;
|
||||
connectionStore.activeConnectionId = connection_id;
|
||||
await connectionStore.ensureConnected(connection_id);
|
||||
if (config.db_type === "redis") {
|
||||
queryStore.createTab(connection_id, database || "0", `db${database || "0"}`, "redis");
|
||||
} else if (config.db_type === "mongodb") {
|
||||
queryStore.createTab(connection_id, database, table, "mongo");
|
||||
} else {
|
||||
deps.openTableTarget({
|
||||
connectionId: connection_id,
|
||||
database,
|
||||
schema,
|
||||
tableName: table,
|
||||
});
|
||||
}
|
||||
const tabId = queryStore.createTab(connection_id, database, undefined, "query");
|
||||
queryStore.updateSql(tabId, sql);
|
||||
await queryStore.executeTabSql(tabId, sql);
|
||||
import("@tauri-apps/api/window").then(({ getCurrentWindow }) =>
|
||||
getCurrentWindow()
|
||||
.setFocus()
|
||||
.catch(() => {}),
|
||||
);
|
||||
},
|
||||
);
|
||||
listen("mcp-reload-connections", async () => {
|
||||
await connectionStore.initFromDisk();
|
||||
});
|
||||
listen<{ connection_id: string; database: string; sql: string }>("mcp-execute-query", async (event) => {
|
||||
const { connection_id, database, sql } = event.payload;
|
||||
if (!connectionStore.connections.length) await connectionStore.initFromDisk();
|
||||
const config = connectionStore.getConfig(connection_id);
|
||||
if (!config) return;
|
||||
connectionStore.activeConnectionId = connection_id;
|
||||
await connectionStore.ensureConnected(connection_id);
|
||||
const tabId = queryStore.createTab(connection_id, database, undefined, "query");
|
||||
queryStore.updateSql(tabId, sql);
|
||||
await queryStore.executeTabSql(tabId, sql);
|
||||
import("@tauri-apps/api/window").then(({ getCurrentWindow }) =>
|
||||
getCurrentWindow()
|
||||
.setFocus()
|
||||
.catch(() => {}),
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("[DBX] mcp-execute-query error:", e);
|
||||
}
|
||||
}).then((unlisten) => unlistenHandles.push(unlisten));
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
return { setupTauriListeners };
|
||||
function cleanupTauriListeners() {
|
||||
unlistenHandles.forEach((unlisten) => unlisten());
|
||||
unlistenHandles.length = 0;
|
||||
}
|
||||
|
||||
return { setupTauriListeners, cleanupTauriListeners };
|
||||
}
|
||||
|
||||
@@ -984,6 +984,17 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
return isSchemaAware(getConfig(connectionId)?.db_type);
|
||||
}
|
||||
|
||||
const COMPLETION_CACHE_MAX = 50;
|
||||
|
||||
function evictOldestCacheEntries(cache: Record<string, unknown>, max: number) {
|
||||
const keys = Object.keys(cache);
|
||||
if (keys.length <= max) return;
|
||||
const toRemove = keys.slice(0, keys.length - max);
|
||||
for (const key of toRemove) {
|
||||
delete cache[key];
|
||||
}
|
||||
}
|
||||
|
||||
async function listCompletionTables(connectionId: string, database: string): Promise<SqlCompletionTable[]> {
|
||||
const cacheKey = `${connectionId}:${database}`;
|
||||
if (completionTablesCache.value[cacheKey]) {
|
||||
@@ -1009,6 +1020,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
}),
|
||||
);
|
||||
completionTablesCache.value[cacheKey] = tableGroups.flat();
|
||||
evictOldestCacheEntries(completionTablesCache.value, COMPLETION_CACHE_MAX);
|
||||
return completionTablesCache.value[cacheKey];
|
||||
}
|
||||
|
||||
@@ -1017,6 +1029,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
name: table.name,
|
||||
type: table.table_type === "VIEW" ? ("view" as const) : ("table" as const),
|
||||
}));
|
||||
evictOldestCacheEntries(completionTablesCache.value, COMPLETION_CACHE_MAX);
|
||||
return completionTablesCache.value[cacheKey];
|
||||
}
|
||||
|
||||
@@ -1031,6 +1044,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
await ensureConnected(connectionId);
|
||||
const querySchema = schema || database;
|
||||
completionColumnsCache.value[cacheKey] = await api.getColumns(connectionId, database, querySchema, table);
|
||||
evictOldestCacheEntries(completionColumnsCache.value, COMPLETION_CACHE_MAX);
|
||||
}
|
||||
|
||||
return completionColumnsCache.value[cacheKey].map((column) => ({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { uuid } from "@/lib/utils";
|
||||
import { ref, watch } from "vue";
|
||||
import { ref, watch, computed } from "vue";
|
||||
import type { DatabaseType, QueryTab } from "@/types/database";
|
||||
import { orderPinnedFirst } from "@/lib/pinnedItems";
|
||||
import { canCancelQueryExecution } from "@/lib/queryExecutionState";
|
||||
@@ -39,9 +39,25 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const activeTabId = ref<string | null>(restored.activeTabId);
|
||||
const MAX_CACHED_RESULTS = 10;
|
||||
|
||||
const _persistSnapshot = computed(() =>
|
||||
tabs.value.map((t) => ({
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
connectionId: t.connectionId,
|
||||
database: t.database,
|
||||
schema: t.schema,
|
||||
sql: t.sql,
|
||||
savedSqlId: t.savedSqlId,
|
||||
pinned: t.pinned,
|
||||
mode: t.mode,
|
||||
objectBrowser: t.objectBrowser,
|
||||
tableMeta: t.tableMeta,
|
||||
})),
|
||||
);
|
||||
|
||||
let _persistTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
watch(
|
||||
[tabs, activeTabId],
|
||||
[_persistSnapshot, activeTabId],
|
||||
() => {
|
||||
if (_persistTimer) clearTimeout(_persistTimer);
|
||||
_persistTimer = setTimeout(() => {
|
||||
@@ -49,7 +65,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
_persistTimer = null;
|
||||
}, 300);
|
||||
},
|
||||
{ deep: true },
|
||||
{ flush: "post" },
|
||||
);
|
||||
|
||||
function findTabByTitle(connectionId: string, database: string, title: string) {
|
||||
@@ -123,6 +139,8 @@ export const useQueryStore = defineStore("query", () => {
|
||||
if (idx < 0) return;
|
||||
if (tabs.value[idx].isExecuting) void cancelTabExecution(id);
|
||||
if (tabs.value[idx].isExplaining) void cancelTabExplain(id);
|
||||
tabs.value[idx].result = undefined;
|
||||
tabs.value[idx].results = undefined;
|
||||
tabs.value.splice(idx, 1);
|
||||
if (activeTabId.value === id) {
|
||||
activeTabId.value = tabs.value[Math.min(idx, tabs.value.length - 1)]?.id ?? null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
import { ref } from "vue";
|
||||
import { uuid } from "@/lib/utils";
|
||||
import * as api from "@/lib/api";
|
||||
import type { SavedSqlFile, SavedSqlFolder, SavedSqlLibrary } from "@/types/database";
|
||||
@@ -34,13 +34,15 @@ export const useSavedSqlStore = defineStore("savedSql", () => {
|
||||
const files = ref<SavedSqlFile[]>([]);
|
||||
const isLoaded = ref(false);
|
||||
|
||||
const version = computed(
|
||||
() => `${folders.value.length}:${files.value.length}:${files.value.map((f) => f.updatedAt).join("|")}`,
|
||||
);
|
||||
const version = ref(0);
|
||||
function bumpVersion() {
|
||||
version.value++;
|
||||
}
|
||||
|
||||
function applyLibrary(library: SavedSqlLibrary) {
|
||||
folders.value = library.folders;
|
||||
files.value = library.files;
|
||||
bumpVersion();
|
||||
}
|
||||
|
||||
async function migrateLegacyLocalStorage() {
|
||||
@@ -89,6 +91,7 @@ export const useSavedSqlStore = defineStore("savedSql", () => {
|
||||
};
|
||||
const saved = await api.saveSavedSqlFolder(folder);
|
||||
folders.value = [...folders.value.filter((item) => item.id !== saved.id), saved];
|
||||
bumpVersion();
|
||||
return saved;
|
||||
}
|
||||
|
||||
@@ -97,12 +100,14 @@ export const useSavedSqlStore = defineStore("savedSql", () => {
|
||||
if (!existing) return;
|
||||
const saved = await api.saveSavedSqlFolder({ ...existing, name, updatedAt: nowIso() });
|
||||
folders.value = folders.value.map((folder) => (folder.id === id ? saved : folder));
|
||||
bumpVersion();
|
||||
}
|
||||
|
||||
async function deleteFolder(id: string) {
|
||||
await api.deleteSavedSqlFolder(id);
|
||||
folders.value = folders.value.filter((folder) => folder.id !== id);
|
||||
files.value = files.value.filter((file) => file.folderId !== id);
|
||||
bumpVersion();
|
||||
}
|
||||
|
||||
async function saveFile(input: {
|
||||
@@ -139,6 +144,7 @@ export const useSavedSqlStore = defineStore("savedSql", () => {
|
||||
};
|
||||
const saved = await api.saveSavedSqlFile(file);
|
||||
files.value = [...files.value.filter((item) => item.id !== saved.id), saved];
|
||||
bumpVersion();
|
||||
return saved;
|
||||
}
|
||||
|
||||
@@ -147,11 +153,13 @@ export const useSavedSqlStore = defineStore("savedSql", () => {
|
||||
if (!existing) return;
|
||||
const saved = await api.saveSavedSqlFile({ ...existing, name, updatedAt: nowIso() });
|
||||
files.value = files.value.map((file) => (file.id === id ? saved : file));
|
||||
bumpVersion();
|
||||
}
|
||||
|
||||
async function deleteFile(id: string) {
|
||||
await api.deleteSavedSqlFile(id);
|
||||
files.value = files.value.filter((file) => file.id !== id);
|
||||
bumpVersion();
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user