import { defineStore } from "pinia"; import { ref } from "vue"; import type { ConnectionConfig, TreeNode } from "@/types/database"; import { orderPinnedFirst } from "@/lib/pinnedItems"; import * as api from "@/lib/tauri"; const PINNED_TREE_NODES_STORAGE_KEY = "dbx-pinned-tree-nodes"; export const useConnectionStore = defineStore("connection", () => { const connections = ref([]); const activeConnectionId = ref(null); const treeNodes = ref([]); const pinnedTreeNodeIds = ref>(loadPinnedTreeNodeIds()); const connectedIds = ref>(new Set()); const editingConnectionId = ref(null); function startEditing(id: string) { editingConnectionId.value = id; } function stopEditing() { editingConnectionId.value = null; } function getConfig(connectionId: string) { return connections.value.find((c) => c.id === connectionId); } function normalizeConnection(config: ConnectionConfig): ConnectionConfig { const labelMap: Record = { mysql: "MySQL", postgres: "PostgreSQL", sqlite: "SQLite", redis: "Redis", duckdb: "DuckDB", clickhouse: "ClickHouse", sqlserver: "SQL Server", mongodb: "MongoDB", oracle: "Oracle", }; return { ...config, driver_profile: config.driver_profile || config.db_type, driver_label: config.driver_label || labelMap[config.driver_profile || config.db_type] || config.db_type, url_params: config.url_params || "", }; } function loadPinnedTreeNodeIds(): Set { try { if (typeof localStorage === "undefined") return new Set(); const saved = localStorage.getItem(PINNED_TREE_NODES_STORAGE_KEY); const ids = saved ? JSON.parse(saved) : []; return new Set(Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : []); } catch { return new Set(); } } function persistPinnedTreeNodeIds() { if (typeof localStorage === "undefined") return; localStorage.setItem(PINNED_TREE_NODES_STORAGE_KEY, JSON.stringify([...pinnedTreeNodeIds.value])); } function isTreeNodePinned(id: string): boolean { return pinnedTreeNodeIds.value.has(id); } function pinTreeNode(node: TreeNode): TreeNode { node.pinned = isTreeNodePinned(node.id); return node; } function setChildren(parent: TreeNode, children: TreeNode[]) { parent.children = orderPinnedFirst(children.map(pinTreeNode), (node) => !!node.pinned); } function findParentNode(nodes: TreeNode[], id: string, parent: TreeNode | null = null): TreeNode | null { for (const node of nodes) { if (node.id === id) return parent; if (node.children) { const found = findParentNode(node.children, id, node); if (found) return found; } } return null; } function toggleTreeNodePin(id: string) { const next = new Set(pinnedTreeNodeIds.value); if (next.has(id)) next.delete(id); else next.add(id); pinnedTreeNodeIds.value = next; persistPinnedTreeNodeIds(); const node = findNode(treeNodes.value, id); if (node) node.pinned = next.has(id); const parent = findParentNode(treeNodes.value, id); if (parent?.children) { parent.children = orderPinnedFirst(parent.children, (child) => !!child.pinned); } else { treeNodes.value = orderPinnedFirst(treeNodes.value, (child) => !!child.pinned); } } function addConnection(config: ConnectionConfig) { connections.value.push(normalizeConnection(config)); persistConnections(); } function removeConnection(id: string) { connections.value = connections.value.filter((c) => c.id !== id); treeNodes.value = treeNodes.value.filter((n) => n.id !== id); if (activeConnectionId.value === id) { activeConnectionId.value = null; } persistConnections(); } function updateConnection(config: ConnectionConfig) { config = normalizeConnection(config); const idx = connections.value.findIndex((c) => c.id === config.id); if (idx >= 0) connections.value[idx] = config; const node = findNode(treeNodes.value, config.id); if (node) { node.label = config.name; node.isExpanded = false; node.children = []; } connectedIds.value.delete(config.id); persistConnections(); } async function connect(config: ConnectionConfig) { config = normalizeConnection(config); const id = await api.connectDb(config); activeConnectionId.value = id; connectedIds.value.add(id); const node: TreeNode = { id, label: config.name, type: "connection", connectionId: id, isExpanded: false, children: [], }; const existing = treeNodes.value.findIndex((n) => n.id === id); if (existing >= 0) { treeNodes.value[existing] = node; } else { treeNodes.value.push(node); } return id; } async function disconnect(connectionId: string) { await api.disconnectDb(connectionId); connectedIds.value.delete(connectionId); const node = treeNodes.value.find((n) => n.connectionId === connectionId); if (node) { node.isExpanded = false; node.children = []; } if (activeConnectionId.value === connectionId) { activeConnectionId.value = null; } } async function ensureConnected(connectionId: string) { if (connectedIds.value.has(connectionId)) return; const config = getConfig(connectionId); if (!config) throw new Error("Connection config not found"); await api.connectDb(config); connectedIds.value.add(connectionId); activeConnectionId.value = connectionId; } async function loadDatabases(connectionId: string) { const node = findNode(treeNodes.value, connectionId); if (!node) return; node.isLoading = true; try { await ensureConnected(connectionId); const databases = await api.listDatabases(connectionId); setChildren(node, databases.map((db) => ({ id: `${connectionId}:${db.name}`, label: db.name, type: "database" as const, connectionId, database: db.name, isExpanded: false, children: [], }))); node.isExpanded = true; } finally { node.isLoading = false; } } async function loadRedisDatabases(connectionId: string) { const node = findNode(treeNodes.value, connectionId); if (!node) return; node.isLoading = true; try { await ensureConnected(connectionId); const dbs = await api.redisListDatabases(connectionId); setChildren(node, dbs.map((db) => ({ id: `${connectionId}:db${db}`, label: `db${db}`, type: "redis-db" as const, connectionId, database: String(db), isExpanded: false, children: [], }))); node.isExpanded = true; } finally { node.isLoading = false; } } async function loadMongoDatabases(connectionId: string) { const node = findNode(treeNodes.value, connectionId); if (!node) return; node.isLoading = true; try { await ensureConnected(connectionId); const dbs = await api.mongoListDatabases(connectionId); setChildren(node, dbs.map((db) => ({ id: `${connectionId}:${db}`, label: db, type: "mongo-db" as const, connectionId, database: db, isExpanded: false, children: [], }))); node.isExpanded = true; } finally { node.isLoading = false; } } async function loadMongoCollections(connectionId: string, database: string) { const nodeId = `${connectionId}:${database}`; const node = findNode(treeNodes.value, nodeId); if (!node) return; node.isLoading = true; try { const collections = await api.mongoListCollections(connectionId, database); setChildren(node, collections.map((col) => ({ id: `${nodeId}:${col}`, label: col, type: "mongo-collection" as const, connectionId, database, isExpanded: false, }))); node.isExpanded = true; } finally { node.isLoading = false; } } async function loadSchemas(connectionId: string, database: string) { const nodeId = `${connectionId}:${database}`; const node = findNode(treeNodes.value, nodeId); if (!node) return; node.isLoading = true; try { const schemas = await api.listSchemas(connectionId, database); setChildren(node, schemas.map((s) => ({ id: `${connectionId}:${database}:${s}`, label: s, type: "schema" as const, connectionId, database, schema: s, isExpanded: false, children: [], }))); node.isExpanded = true; } finally { node.isLoading = false; } } async function loadTables(connectionId: string, database: string, schema?: string) { const nodeId = schema ? `${connectionId}:${database}:${schema}` : `${connectionId}:${database}`; const node = findNode(treeNodes.value, nodeId); if (!node) return; node.isLoading = true; try { const querySchema = schema || database; const tables = await api.listTables(connectionId, database, querySchema); setChildren(node, tables.map((t) => ({ id: `${nodeId}:${t.name}`, label: t.name, type: (t.table_type === "VIEW" ? "view" : "table") as "view" | "table", connectionId, database, schema, isExpanded: false, children: [], }))); node.isExpanded = true; } finally { node.isLoading = false; } } async function loadTableGroups(connectionId: string, database: string, table: string, schema?: string) { const parentId = schema ? `${connectionId}:${database}:${schema}:${table}` : `${connectionId}:${database}:${table}`; const node = findNode(treeNodes.value, parentId); if (!node) return; setChildren(node, [ { id: `${parentId}:__columns`, label: "tree.columns", type: "group-columns", connectionId, database, schema, tableName: table, isExpanded: false, children: [] }, { id: `${parentId}:__indexes`, label: "tree.indexes", type: "group-indexes", connectionId, database, schema, tableName: table, isExpanded: false, children: [] }, { id: `${parentId}:__fkeys`, label: "tree.foreignKeys", type: "group-fkeys", connectionId, database, schema, tableName: table, isExpanded: false, children: [] }, { id: `${parentId}:__triggers`, label: "tree.triggers", type: "group-triggers", connectionId, database, schema, tableName: table, isExpanded: false, children: [] }, ]); node.isExpanded = true; } async function loadColumns(connectionId: string, database: string, table: string, schema?: string) { const parentId = schema ? `${connectionId}:${database}:${schema}:${table}:__columns` : `${connectionId}:${database}:${table}:__columns`; const node = findNode(treeNodes.value, parentId); if (!node) return; node.isLoading = true; try { const querySchema = schema || database; const columns = await api.getColumns(connectionId, database, querySchema, table); setChildren(node, columns.map((col) => ({ id: `${parentId}:${col.name}`, label: `${col.name} (${col.data_type})`, type: "column" as const, connectionId, database, schema, meta: col, }))); node.isExpanded = true; } finally { node.isLoading = false; } } async function loadIndexes(connectionId: string, database: string, table: string, schema?: string) { const parentId = schema ? `${connectionId}:${database}:${schema}:${table}:__indexes` : `${connectionId}:${database}:${table}:__indexes`; const node = findNode(treeNodes.value, parentId); if (!node) return; node.isLoading = true; try { const querySchema = schema || database; const indexes = await api.listIndexes(connectionId, database, querySchema, table); setChildren(node, indexes.map((idx) => ({ id: `${parentId}:${idx.name}`, label: `${idx.name} (${idx.columns.join(", ")})`, type: "index" as const, connectionId, database, schema, meta: idx, }))); node.isExpanded = true; } finally { node.isLoading = false; } } async function loadForeignKeys(connectionId: string, database: string, table: string, schema?: string) { const parentId = schema ? `${connectionId}:${database}:${schema}:${table}:__fkeys` : `${connectionId}:${database}:${table}:__fkeys`; const node = findNode(treeNodes.value, parentId); if (!node) return; node.isLoading = true; try { const querySchema = schema || database; const fkeys = await api.listForeignKeys(connectionId, database, querySchema, table); setChildren(node, fkeys.map((fk) => ({ id: `${parentId}:${fk.name}`, label: `${fk.column} → ${fk.ref_table}.${fk.ref_column}`, type: "fkey" as const, connectionId, database, schema, meta: fk, }))); node.isExpanded = true; } finally { node.isLoading = false; } } async function loadTriggers(connectionId: string, database: string, table: string, schema?: string) { const parentId = schema ? `${connectionId}:${database}:${schema}:${table}:__triggers` : `${connectionId}:${database}:${table}:__triggers`; const node = findNode(treeNodes.value, parentId); if (!node) return; node.isLoading = true; try { const querySchema = schema || database; const triggers = await api.listTriggers(connectionId, database, querySchema, table); setChildren(node, triggers.map((tr) => ({ id: `${parentId}:${tr.name}`, label: `${tr.name} (${tr.timing} ${tr.event})`, type: "trigger" as const, connectionId, database, schema, meta: tr, }))); node.isExpanded = true; } finally { node.isLoading = false; } } function findNode(nodes: TreeNode[], id: string): TreeNode | null { for (const node of nodes) { if (node.id === id) return node; if (node.children) { const found = findNode(node.children, id); if (found) return found; } } return null; } function persistConnections() { api.saveConnections(connections.value).catch(() => {}); } async function exportConnectionsToFile() { const { save } = await import("@tauri-apps/plugin-dialog"); const { writeTextFile } = await import("@tauri-apps/plugin-fs"); const path = await save({ filters: [{ name: "JSON", extensions: ["json"] }], defaultPath: "dbx-connections.json" }); if (!path) return; const data = connections.value.map((c) => ({ ...c, password: "", ssh_password: "" })); await writeTextFile(path, JSON.stringify(data, null, 2)); } async function importConnectionsFromFile() { const { open } = await import("@tauri-apps/plugin-dialog"); const { readTextFile } = await import("@tauri-apps/plugin-fs"); const path = await open({ filters: [{ name: "JSON", extensions: ["json"] }], multiple: false }); if (!path) return; const json = await readTextFile(path as string); const imported: ConnectionConfig[] = JSON.parse(json); for (const config of imported) { if (!connections.value.find((c) => c.id === config.id)) { config.id = crypto.randomUUID(); const normalized = normalizeConnection(config); addConnection(normalized); treeNodes.value.push({ id: normalized.id, label: normalized.name, type: "connection" as const, connectionId: normalized.id, isExpanded: false, children: [], }); } } } async function initFromDisk() { const saved = await api.loadConnections(); connections.value = saved.map(normalizeConnection); treeNodes.value = saved.map((config) => ({ id: config.id, label: config.name, type: "connection" as const, connectionId: config.id, isExpanded: false, children: [], })); } return { connections, activeConnectionId, treeNodes, connectedIds, getConfig, isTreeNodePinned, toggleTreeNodePin, addConnection, updateConnection, removeConnection, editingConnectionId, startEditing, stopEditing, connect, disconnect, ensureConnected, initFromDisk, loadDatabases, loadRedisDatabases, loadMongoDatabases, loadMongoCollections, loadSchemas, loadTables, loadTableGroups, loadColumns, loadIndexes, loadForeignKeys, loadTriggers, exportConnectionsToFile, importConnectionsFromFile, }; });