Files
dbx/src/stores/savedSqlStore.ts
T
t8y2 f456ddd979 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
2026-05-11 00:18:09 +08:00

182 lines
5.2 KiB
TypeScript

import { defineStore } from "pinia";
import { ref } from "vue";
import { uuid } from "@/lib/utils";
import * as api from "@/lib/api";
import type { SavedSqlFile, SavedSqlFolder, SavedSqlLibrary } from "@/types/database";
const LEGACY_STORAGE_KEY = "dbx-saved-sql-library";
interface SavedSqlState {
folders: SavedSqlFolder[];
files: SavedSqlFile[];
}
function nowIso() {
return new Date().toISOString();
}
function loadLegacyState(): SavedSqlState {
try {
const raw = localStorage.getItem(LEGACY_STORAGE_KEY);
if (!raw) return { folders: [], files: [] };
const parsed = JSON.parse(raw) as Partial<SavedSqlState>;
return {
folders: Array.isArray(parsed.folders) ? parsed.folders.filter((item) => item?.id && item?.connectionId) : [],
files: Array.isArray(parsed.files) ? parsed.files.filter((item) => item?.id && item?.connectionId) : [],
};
} catch {
return { folders: [], files: [] };
}
}
export const useSavedSqlStore = defineStore("savedSql", () => {
const folders = ref<SavedSqlFolder[]>([]);
const files = ref<SavedSqlFile[]>([]);
const isLoaded = ref(false);
const version = ref(0);
function bumpVersion() {
version.value++;
}
function applyLibrary(library: SavedSqlLibrary) {
folders.value = library.folders;
files.value = library.files;
bumpVersion();
}
async function migrateLegacyLocalStorage() {
const legacy = loadLegacyState();
if (legacy.folders.length === 0 && legacy.files.length === 0) return;
for (const folder of legacy.folders) {
await api.saveSavedSqlFolder(folder);
}
for (const file of legacy.files) {
await api.saveSavedSqlFile(file);
}
localStorage.removeItem(LEGACY_STORAGE_KEY);
}
async function initFromStorage() {
await migrateLegacyLocalStorage();
applyLibrary(await api.loadSavedSqlLibrary());
isLoaded.value = true;
}
function listFolders(connectionId: string) {
return folders.value
.filter((folder) => folder.connectionId === connectionId)
.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: "base" }));
}
function listFiles(connectionId: string, folderId?: string) {
return files.value
.filter((file) => file.connectionId === connectionId && (file.folderId || "") === (folderId || ""))
.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: "base" }));
}
function getFile(id: string) {
return files.value.find((file) => file.id === id);
}
async function createFolder(connectionId: string, name: string) {
const timestamp = nowIso();
const folder: SavedSqlFolder = {
id: uuid(),
connectionId,
name,
createdAt: timestamp,
updatedAt: timestamp,
};
const saved = await api.saveSavedSqlFolder(folder);
folders.value = [...folders.value.filter((item) => item.id !== saved.id), saved];
bumpVersion();
return saved;
}
async function renameFolder(id: string, name: string) {
const existing = folders.value.find((folder) => folder.id === id);
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: {
id?: string;
connectionId: string;
folderId?: string;
name: string;
database: string;
schema?: string;
sql: string;
}) {
const timestamp = nowIso();
const existing = input.id ? getFile(input.id) : undefined;
const file: SavedSqlFile = existing
? {
...existing,
folderId: input.folderId || undefined,
name: input.name,
database: input.database,
schema: input.schema,
sql: input.sql,
updatedAt: timestamp,
}
: {
id: uuid(),
connectionId: input.connectionId,
folderId: input.folderId || undefined,
name: input.name,
database: input.database,
schema: input.schema,
sql: input.sql,
createdAt: timestamp,
updatedAt: timestamp,
};
const saved = await api.saveSavedSqlFile(file);
files.value = [...files.value.filter((item) => item.id !== saved.id), saved];
bumpVersion();
return saved;
}
async function renameFile(id: string, name: string) {
const existing = getFile(id);
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 {
folders,
files,
isLoaded,
version,
initFromStorage,
listFolders,
listFiles,
getFile,
createFolder,
renameFolder,
deleteFolder,
saveFile,
renameFile,
deleteFile,
};
});