diff --git a/crates/dbx-core/src/lib.rs b/crates/dbx-core/src/lib.rs index 20b1d6c76..08eece7f2 100644 --- a/crates/dbx-core/src/lib.rs +++ b/crates/dbx-core/src/lib.rs @@ -8,6 +8,7 @@ pub mod mongo_ops; pub mod query; pub mod query_cancel; pub mod redis_ops; +pub mod saved_sql; pub mod schema; pub mod sql; pub mod storage; diff --git a/crates/dbx-core/src/saved_sql.rs b/crates/dbx-core/src/saved_sql.rs new file mode 100644 index 000000000..7f096d470 --- /dev/null +++ b/crates/dbx-core/src/saved_sql.rs @@ -0,0 +1,32 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SavedSqlFolder { + pub id: String, + pub connection_id: String, + pub name: String, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SavedSqlFile { + pub id: String, + pub connection_id: String, + pub folder_id: Option, + pub name: String, + pub database: String, + pub schema: Option, + pub sql: String, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SavedSqlLibrary { + pub folders: Vec, + pub files: Vec, +} diff --git a/crates/dbx-core/src/storage.rs b/crates/dbx-core/src/storage.rs index e8058a5de..98c06af72 100644 --- a/crates/dbx-core/src/storage.rs +++ b/crates/dbx-core/src/storage.rs @@ -6,6 +6,7 @@ use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions}; use crate::ai::{AiChatMessage, AiConfig, AiConversation}; use crate::history::HistoryEntry; use crate::models::connection::ConnectionConfig; +use crate::saved_sql::{SavedSqlFile, SavedSqlFolder, SavedSqlLibrary}; pub struct Storage { db: SqlitePool, @@ -58,6 +59,24 @@ const SCHEMA_STATEMENTS: &[&str] = &[ payload_json TEXT NOT NULL, updated_at TEXT NOT NULL )", + "CREATE TABLE IF NOT EXISTS saved_sql_folders ( + id TEXT PRIMARY KEY, + connection_id TEXT NOT NULL, + name TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT '', + updated_at TEXT NOT NULL DEFAULT '' + )", + "CREATE TABLE IF NOT EXISTS saved_sql_files ( + id TEXT PRIMARY KEY, + connection_id TEXT NOT NULL, + folder_id TEXT, + name TEXT NOT NULL DEFAULT '', + database_name TEXT NOT NULL DEFAULT '', + schema_name TEXT, + sql_text TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT '', + updated_at TEXT NOT NULL DEFAULT '' + )", ]; // --------------------------------------------------------------------------- @@ -381,6 +400,159 @@ impl Storage { } } +// --------------------------------------------------------------------------- +// Saved SQL Library +// --------------------------------------------------------------------------- + +#[derive(sqlx::FromRow)] +struct SavedSqlFolderRow { + id: String, + connection_id: String, + name: String, + created_at: String, + updated_at: String, +} + +#[derive(sqlx::FromRow)] +struct SavedSqlFileRow { + id: String, + connection_id: String, + folder_id: Option, + name: String, + database_name: String, + schema_name: Option, + sql_text: String, + created_at: String, + updated_at: String, +} + +impl From for SavedSqlFolder { + fn from(row: SavedSqlFolderRow) -> Self { + Self { + id: row.id, + connection_id: row.connection_id, + name: row.name, + created_at: row.created_at, + updated_at: row.updated_at, + } + } +} + +impl From for SavedSqlFile { + fn from(row: SavedSqlFileRow) -> Self { + Self { + id: row.id, + connection_id: row.connection_id, + folder_id: row.folder_id, + name: row.name, + database: row.database_name, + schema: row.schema_name, + sql: row.sql_text, + created_at: row.created_at, + updated_at: row.updated_at, + } + } +} + +impl Storage { + pub async fn load_saved_sql_library(&self) -> Result { + let folder_rows: Vec = sqlx::query_as( + "SELECT id, connection_id, name, created_at, updated_at \ + FROM saved_sql_folders ORDER BY connection_id, name COLLATE NOCASE", + ) + .fetch_all(&self.db) + .await + .map_err(|e| e.to_string())?; + + let file_rows: Vec = sqlx::query_as( + "SELECT id, connection_id, folder_id, name, database_name, schema_name, sql_text, created_at, updated_at \ + FROM saved_sql_files ORDER BY connection_id, folder_id, name COLLATE NOCASE", + ) + .fetch_all(&self.db) + .await + .map_err(|e| e.to_string())?; + + Ok(SavedSqlLibrary { + folders: folder_rows.into_iter().map(Into::into).collect(), + files: file_rows.into_iter().map(Into::into).collect(), + }) + } + + pub async fn save_saved_sql_folder(&self, folder: &SavedSqlFolder) -> Result<(), String> { + sqlx::query( + "INSERT INTO saved_sql_folders (id, connection_id, name, created_at, updated_at) \ + VALUES (?, ?, ?, ?, ?) \ + ON CONFLICT(id) DO UPDATE SET \ + connection_id = excluded.connection_id, \ + name = excluded.name, \ + updated_at = excluded.updated_at", + ) + .bind(&folder.id) + .bind(&folder.connection_id) + .bind(&folder.name) + .bind(&folder.created_at) + .bind(&folder.updated_at) + .execute(&self.db) + .await + .map_err(|e| e.to_string())?; + Ok(()) + } + + pub async fn delete_saved_sql_folder(&self, id: &str) -> Result<(), String> { + let mut tx = self.db.begin().await.map_err(|e| e.to_string())?; + sqlx::query("DELETE FROM saved_sql_files WHERE folder_id = ?") + .bind(id) + .execute(&mut *tx) + .await + .map_err(|e| e.to_string())?; + sqlx::query("DELETE FROM saved_sql_folders WHERE id = ?") + .bind(id) + .execute(&mut *tx) + .await + .map_err(|e| e.to_string())?; + tx.commit().await.map_err(|e| e.to_string())?; + Ok(()) + } + + pub async fn save_saved_sql_file(&self, file: &SavedSqlFile) -> Result<(), String> { + sqlx::query( + "INSERT INTO saved_sql_files \ + (id, connection_id, folder_id, name, database_name, schema_name, sql_text, created_at, updated_at) \ + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) \ + ON CONFLICT(id) DO UPDATE SET \ + connection_id = excluded.connection_id, \ + folder_id = excluded.folder_id, \ + name = excluded.name, \ + database_name = excluded.database_name, \ + schema_name = excluded.schema_name, \ + sql_text = excluded.sql_text, \ + updated_at = excluded.updated_at", + ) + .bind(&file.id) + .bind(&file.connection_id) + .bind(&file.folder_id) + .bind(&file.name) + .bind(&file.database) + .bind(&file.schema) + .bind(&file.sql) + .bind(&file.created_at) + .bind(&file.updated_at) + .execute(&self.db) + .await + .map_err(|e| e.to_string())?; + Ok(()) + } + + pub async fn delete_saved_sql_file(&self, id: &str) -> Result<(), String> { + sqlx::query("DELETE FROM saved_sql_files WHERE id = ?") + .bind(id) + .execute(&self.db) + .await + .map_err(|e| e.to_string())?; + Ok(()) + } +} + // --------------------------------------------------------------------------- // Secrets // --------------------------------------------------------------------------- diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 25acd55c5..a45b52baf 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -8,6 +8,7 @@ pub mod mongo_cmd; pub mod query; pub mod query_cancel; pub mod redis_cmd; +pub mod saved_sql; pub mod schema; pub mod schema_cache; pub mod sql_file; diff --git a/src-tauri/src/commands/saved_sql.rs b/src-tauri/src/commands/saved_sql.rs new file mode 100644 index 000000000..01c5c0cda --- /dev/null +++ b/src-tauri/src/commands/saved_sql.rs @@ -0,0 +1,35 @@ +use std::sync::Arc; +use tauri::State; + +use dbx_core::connection::AppState; +use dbx_core::saved_sql::{SavedSqlFile, SavedSqlFolder, SavedSqlLibrary}; + +#[tauri::command] +pub async fn load_saved_sql_library(state: State<'_, Arc>) -> Result { + state.storage.load_saved_sql_library().await +} + +#[tauri::command] +pub async fn save_saved_sql_folder( + state: State<'_, Arc>, + folder: SavedSqlFolder, +) -> Result { + state.storage.save_saved_sql_folder(&folder).await?; + Ok(folder) +} + +#[tauri::command] +pub async fn delete_saved_sql_folder(state: State<'_, Arc>, id: String) -> Result<(), String> { + state.storage.delete_saved_sql_folder(&id).await +} + +#[tauri::command] +pub async fn save_saved_sql_file(state: State<'_, Arc>, file: SavedSqlFile) -> Result { + state.storage.save_saved_sql_file(&file).await?; + Ok(file) +} + +#[tauri::command] +pub async fn delete_saved_sql_file(state: State<'_, Arc>, id: String) -> Result<(), String> { + state.storage.delete_saved_sql_file(&id).await +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 03437166c..2dc56bf08 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -112,6 +112,11 @@ pub fn run() { commands::redis_cmd::redis_set_ttl, commands::redis_cmd::redis_delete_keys, commands::redis_cmd::redis_load_more, + commands::saved_sql::load_saved_sql_library, + commands::saved_sql::save_saved_sql_folder, + commands::saved_sql::delete_saved_sql_folder, + commands::saved_sql::save_saved_sql_file, + commands::saved_sql::delete_saved_sql_file, commands::mongo_cmd::mongo_list_databases, commands::mongo_cmd::mongo_list_collections, commands::mongo_cmd::mongo_find_documents, diff --git a/src-web/src/main.rs b/src-web/src/main.rs index 1c35fa10f..c977c2f30 100644 --- a/src-web/src/main.rs +++ b/src-web/src/main.rs @@ -124,6 +124,14 @@ async fn main() { .route("/history", get(routes::history::load_history).delete(routes::history::clear_history)) .route("/history/save", post(routes::history::save_history)) .route("/history/{id}", delete(routes::history::delete_history_entry)) + // Saved SQL + .route( + "/saved-sql", + get(routes::saved_sql::load_saved_sql_library).post(routes::saved_sql::save_saved_sql_file), + ) + .route("/saved-sql/{id}", delete(routes::saved_sql::delete_saved_sql_file)) + .route("/saved-sql/folders", post(routes::saved_sql::save_saved_sql_folder)) + .route("/saved-sql/folders/{id}", delete(routes::saved_sql::delete_saved_sql_folder)) // AI .route("/ai/config", post(routes::ai::save_ai_config).get(routes::ai::load_ai_config)) .route("/ai/conversation", post(routes::ai::save_ai_conversation)) diff --git a/src-web/src/routes/mod.rs b/src-web/src/routes/mod.rs index 4589bb390..107d20225 100644 --- a/src-web/src/routes/mod.rs +++ b/src-web/src/routes/mod.rs @@ -5,6 +5,7 @@ pub mod layout; pub mod mongo; pub mod query; pub mod redis; +pub mod saved_sql; pub mod schema; pub mod schema_cache; pub mod sql_file; diff --git a/src-web/src/routes/saved_sql.rs b/src-web/src/routes/saved_sql.rs new file mode 100644 index 000000000..dcc62f0bf --- /dev/null +++ b/src-web/src/routes/saved_sql.rs @@ -0,0 +1,45 @@ +use std::sync::Arc; + +use axum::extract::{Path, State}; +use axum::Json; +use dbx_core::saved_sql::{SavedSqlFile, SavedSqlFolder, SavedSqlLibrary}; + +use crate::error::AppError; +use crate::state::WebState; + +pub async fn load_saved_sql_library(State(state): State>) -> Result, AppError> { + let library = state.app.storage.load_saved_sql_library().await.map_err(AppError)?; + Ok(Json(library)) +} + +pub async fn save_saved_sql_folder( + State(state): State>, + Json(folder): Json, +) -> Result, AppError> { + state.app.storage.save_saved_sql_folder(&folder).await.map_err(AppError)?; + Ok(Json(folder)) +} + +pub async fn delete_saved_sql_folder( + State(state): State>, + Path(id): Path, +) -> Result, AppError> { + state.app.storage.delete_saved_sql_folder(&id).await.map_err(AppError)?; + Ok(Json(())) +} + +pub async fn save_saved_sql_file( + State(state): State>, + Json(file): Json, +) -> Result, AppError> { + state.app.storage.save_saved_sql_file(&file).await.map_err(AppError)?; + Ok(Json(file)) +} + +pub async fn delete_saved_sql_file( + State(state): State>, + Path(id): Path, +) -> Result, AppError> { + state.app.storage.delete_saved_sql_file(&id).await.map_err(AppError)?; + Ok(Json(())) +} diff --git a/src/App.vue b/src/App.vue index e35a5c604..4d3690944 100644 --- a/src/App.vue +++ b/src/App.vue @@ -161,12 +161,12 @@ function defaultSavedSqlName(title: string) { return trimmed.endsWith(".sql") ? trimmed : `${trimmed}.sql`; } -function openSaveSqlDialog() { +async function openSaveSqlDialog() { const tab = activeTab.value; if (!tab || !tab.sql.trim()) return; const existing = tab.savedSqlId ? savedSqlStore.getFile(tab.savedSqlId) : undefined; if (existing) { - const updated = savedSqlStore.saveFile({ + const updated = await savedSqlStore.saveFile({ id: existing.id, connectionId: tab.connectionId, folderId: existing.folderId, @@ -186,12 +186,12 @@ function openSaveSqlDialog() { showSaveSqlDialog.value = true; } -function confirmSaveSqlToLibrary() { +async function confirmSaveSqlToLibrary() { const tab = activeTab.value; const name = saveSqlName.value.trim(); if (!tab || !tab.sql.trim() || !name) return; try { - const saved = savedSqlStore.saveFile({ + const saved = await savedSqlStore.saveFile({ id: tab.savedSqlId, connectionId: tab.connectionId, folderId: saveSqlFolderId.value === ROOT_SAVED_SQL_FOLDER ? undefined : saveSqlFolderId.value, @@ -378,7 +378,7 @@ function handleKeydown(e: KeyboardEvent) { ) { e.preventDefault(); e.stopPropagation(); - openSaveSqlDialog(); + void openSaveSqlDialog(); return; } if ( @@ -402,8 +402,9 @@ function onLoginSuccess() { } function initApp() { - connectionStore - .initFromDisk() + savedSqlStore + .initFromStorage() + .then(() => connectionStore.initFromDisk()) .then(() => { reconnectRestoredTabs(); }) @@ -531,7 +532,7 @@ onUnmounted(() => { @cancel="cancelActiveExecution()" @explain="tryExplain()" @format-sql="formatActiveSql" - @save-sql="openSaveSqlDialog" + @save-sql="void openSaveSqlDialog()" @open-sql="openSqlFile" @change-connection="changeActiveConnection" @change-database="changeActiveDatabase" diff --git a/src/components/sidebar/TreeItem.vue b/src/components/sidebar/TreeItem.vue index bccd31657..116251e21 100644 --- a/src/components/sidebar/TreeItem.vue +++ b/src/components/sidebar/TreeItem.vue @@ -1221,16 +1221,16 @@ function openRenameSavedSqlFile() { showSavedSqlNameDialog.value = true; } -function confirmSavedSqlName() { +async function confirmSavedSqlName() { const name = savedSqlNameInput.value.trim(); if (!name || !props.node.connectionId || !savedSqlNameMode.value) return; if (savedSqlNameMode.value === "folder-create") { - savedSqlStore.createFolder(props.node.connectionId, name); + await savedSqlStore.createFolder(props.node.connectionId, name); } else if (savedSqlNameMode.value === "folder-rename" && props.node.savedSqlFolderId) { - savedSqlStore.renameFolder(props.node.savedSqlFolderId, name); + await savedSqlStore.renameFolder(props.node.savedSqlFolderId, name); } else if (savedSqlNameMode.value === "file-rename" && props.node.savedSqlId) { - savedSqlStore.renameFile(props.node.savedSqlId, name.endsWith(".sql") ? name : `${name}.sql`); + await savedSqlStore.renameFile(props.node.savedSqlId, name.endsWith(".sql") ? name : `${name}.sql`); } connectionStore.refreshSavedSqlTree(props.node.connectionId); @@ -1242,9 +1242,9 @@ function deleteSavedSqlFile() { showDeleteSavedSqlFileConfirm.value = true; } -function confirmDeleteSavedSqlFile() { +async function confirmDeleteSavedSqlFile() { if (!props.node.savedSqlId) return; - savedSqlStore.deleteFile(props.node.savedSqlId); + await savedSqlStore.deleteFile(props.node.savedSqlId); connectionStore.refreshSavedSqlTree(props.node.connectionId); showDeleteSavedSqlFileConfirm.value = false; } @@ -1253,9 +1253,9 @@ function deleteSavedSqlFolder() { showDeleteSavedSqlFolderConfirm.value = true; } -function confirmDeleteSavedSqlFolder() { +async function confirmDeleteSavedSqlFolder() { if (!props.node.savedSqlFolderId) return; - savedSqlStore.deleteFolder(props.node.savedSqlFolderId); + await savedSqlStore.deleteFolder(props.node.savedSqlFolderId); connectionStore.refreshSavedSqlTree(props.node.connectionId); showDeleteSavedSqlFolderConfirm.value = false; } diff --git a/src/lib/api.ts b/src/lib/api.ts index 7a54bdcaf..20fb10a23 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -36,6 +36,11 @@ export const connectDb = forward("connectDb"); export const disconnectDb = forward("disconnectDb"); export const saveConnections = forward("saveConnections"); export const loadConnections = forward("loadConnections"); +export const loadSavedSqlLibrary = forward("loadSavedSqlLibrary"); +export const saveSavedSqlFolder = forward("saveSavedSqlFolder"); +export const deleteSavedSqlFolder = forward("deleteSavedSqlFolder"); +export const saveSavedSqlFile = forward("saveSavedSqlFile"); +export const deleteSavedSqlFile = forward("deleteSavedSqlFile"); // Schema export const listDatabases = forward("listDatabases"); diff --git a/src/lib/http.ts b/src/lib/http.ts index 72811955a..085e187a5 100644 --- a/src/lib/http.ts +++ b/src/lib/http.ts @@ -8,6 +8,9 @@ import type { TriggerInfo, QueryResult, SidebarLayout, + SavedSqlFile, + SavedSqlFolder, + SavedSqlLibrary, } from "@/types/database"; import type { AiConfig } from "@/stores/settingsStore"; import type { @@ -88,6 +91,26 @@ export async function loadConnections(): Promise { return get("/api/connection/list"); } +export async function loadSavedSqlLibrary(): Promise { + return get("/api/saved-sql"); +} + +export async function saveSavedSqlFolder(folder: SavedSqlFolder): Promise { + return post("/api/saved-sql/folders", folder); +} + +export async function deleteSavedSqlFolder(id: string): Promise { + return del(`/api/saved-sql/folders/${encodeURIComponent(id)}`); +} + +export async function saveSavedSqlFile(file: SavedSqlFile): Promise { + return post("/api/saved-sql", file); +} + +export async function deleteSavedSqlFile(id: string): Promise { + return del(`/api/saved-sql/${encodeURIComponent(id)}`); +} + // --------------------------------------------------------------------------- // Schema // --------------------------------------------------------------------------- diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index a04b72a8f..6bd8dfa65 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -9,6 +9,9 @@ import type { ForeignKeyInfo, TriggerInfo, QueryResult, + SavedSqlFile, + SavedSqlFolder, + SavedSqlLibrary, } from "@/types/database"; import type { AiConfig } from "@/stores/settingsStore"; @@ -241,6 +244,26 @@ export async function loadConnections(): Promise { return invoke("load_connections"); } +export async function loadSavedSqlLibrary(): Promise { + return invoke("load_saved_sql_library"); +} + +export async function saveSavedSqlFolder(folder: SavedSqlFolder): Promise { + return invoke("save_saved_sql_folder", { folder }); +} + +export async function deleteSavedSqlFolder(id: string): Promise { + return invoke("delete_saved_sql_folder", { id }); +} + +export async function saveSavedSqlFile(file: SavedSqlFile): Promise { + return invoke("save_saved_sql_file", { file }); +} + +export async function deleteSavedSqlFile(id: string): Promise { + return invoke("delete_saved_sql_file", { id }); +} + export async function saveSidebarLayout(layout: import("@/types/database").SidebarLayout): Promise { return invoke("save_sidebar_layout", { layout }); } diff --git a/src/stores/queryStore.ts b/src/stores/queryStore.ts index 4ec4b6082..685419f8c 100644 --- a/src/stores/queryStore.ts +++ b/src/stores/queryStore.ts @@ -11,7 +11,7 @@ import { restoreOpenTabsState, serializeOpenTabs } from "@/lib/openTabsPersisten import * as api from "@/lib/api"; import { useConnectionStore } from "@/stores/connectionStore"; import { isTauriRuntime } from "@/lib/tauriRuntime"; -import type { SavedSqlFile } from "@/stores/savedSqlStore"; +import type { SavedSqlFile } from "@/types/database"; const STORAGE_KEY = "dbx-open-tabs"; const ACTIVE_TAB_KEY = "dbx-active-tab"; diff --git a/src/stores/savedSqlStore.ts b/src/stores/savedSqlStore.ts index 0d6487fcb..5258f35a2 100644 --- a/src/stores/savedSqlStore.ts +++ b/src/stores/savedSqlStore.ts @@ -1,28 +1,10 @@ import { defineStore } from "pinia"; import { computed, ref } from "vue"; import { uuid } from "@/lib/utils"; +import * as api from "@/lib/api"; +import type { SavedSqlFile, SavedSqlFolder, SavedSqlLibrary } from "@/types/database"; -const STORAGE_KEY = "dbx-saved-sql-library"; - -export interface SavedSqlFolder { - id: string; - connectionId: string; - name: string; - createdAt: string; - updatedAt: string; -} - -export interface SavedSqlFile { - id: string; - connectionId: string; - folderId?: string; - name: string; - database: string; - schema?: string; - sql: string; - createdAt: string; - updatedAt: string; -} +const LEGACY_STORAGE_KEY = "dbx-saved-sql-library"; interface SavedSqlState { folders: SavedSqlFolder[]; @@ -33,9 +15,9 @@ function nowIso() { return new Date().toISOString(); } -function loadState(): SavedSqlState { +function loadLegacyState(): SavedSqlState { try { - const raw = localStorage.getItem(STORAGE_KEY); + const raw = localStorage.getItem(LEGACY_STORAGE_KEY); if (!raw) return { folders: [], files: [] }; const parsed = JSON.parse(raw) as Partial; return { @@ -48,16 +30,36 @@ function loadState(): SavedSqlState { } export const useSavedSqlStore = defineStore("savedSql", () => { - const initial = loadState(); - const folders = ref(initial.folders); - const files = ref(initial.files); + const folders = ref([]); + const files = ref([]); + const isLoaded = ref(false); const version = computed( () => `${folders.value.length}:${files.value.length}:${files.value.map((f) => f.updatedAt).join("|")}`, ); - function persist() { - localStorage.setItem(STORAGE_KEY, JSON.stringify({ folders: folders.value, files: files.value })); + function applyLibrary(library: SavedSqlLibrary) { + folders.value = library.folders; + files.value = library.files; + } + + 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) { @@ -76,7 +78,7 @@ export const useSavedSqlStore = defineStore("savedSql", () => { return files.value.find((file) => file.id === id); } - function createFolder(connectionId: string, name: string) { + async function createFolder(connectionId: string, name: string) { const timestamp = nowIso(); const folder: SavedSqlFolder = { id: uuid(), @@ -85,26 +87,25 @@ export const useSavedSqlStore = defineStore("savedSql", () => { createdAt: timestamp, updatedAt: timestamp, }; - folders.value = [...folders.value, folder]; - persist(); - return folder; + const saved = await api.saveSavedSqlFolder(folder); + folders.value = [...folders.value.filter((item) => item.id !== saved.id), saved]; + return saved; } - function renameFolder(id: string, name: string) { - const timestamp = nowIso(); - folders.value = folders.value.map((folder) => - folder.id === id ? { ...folder, name, updatedAt: timestamp } : folder, - ); - persist(); + 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)); } - function deleteFolder(id: string) { + 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); - persist(); } - function saveFile(input: { + async function saveFile(input: { id?: string; connectionId: string; folderId?: string; @@ -114,10 +115,9 @@ export const useSavedSqlStore = defineStore("savedSql", () => { sql: string; }) { const timestamp = nowIso(); - if (input.id) { - const existing = getFile(input.id); - if (existing) { - const updated: SavedSqlFile = { + const existing = input.id ? getFile(input.id) : undefined; + const file: SavedSqlFile = existing + ? { ...existing, folderId: input.folderId || undefined, name: input.name, @@ -125,44 +125,41 @@ export const useSavedSqlStore = defineStore("savedSql", () => { 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, }; - files.value = files.value.map((file) => (file.id === input.id ? updated : file)); - persist(); - return updated; - } - } - - const file: SavedSqlFile = { - 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, - }; - files.value = [...files.value, file]; - persist(); - return file; + const saved = await api.saveSavedSqlFile(file); + files.value = [...files.value.filter((item) => item.id !== saved.id), saved]; + return saved; } - function renameFile(id: string, name: string) { - const timestamp = nowIso(); - files.value = files.value.map((file) => (file.id === id ? { ...file, name, updatedAt: timestamp } : file)); - persist(); + 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)); } - function deleteFile(id: string) { + async function deleteFile(id: string) { + await api.deleteSavedSqlFile(id); files.value = files.value.filter((file) => file.id !== id); - persist(); } return { folders, files, + isLoaded, version, + initFromStorage, listFolders, listFiles, getFile, diff --git a/src/types/database.ts b/src/types/database.ts index 7bd036d70..3fcedf7e3 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -188,3 +188,28 @@ export interface QueryTab { columns: string[]; }; } + +export interface SavedSqlFolder { + id: string; + connectionId: string; + name: string; + createdAt: string; + updatedAt: string; +} + +export interface SavedSqlFile { + id: string; + connectionId: string; + folderId?: string; + name: string; + database: string; + schema?: string; + sql: string; + createdAt: string; + updatedAt: string; +} + +export interface SavedSqlLibrary { + folders: SavedSqlFolder[]; + files: SavedSqlFile[]; +}