feat(sql): persist SQL library in app database
This commit is contained in:
parent
cac383e6bf
commit
b013a2ac29
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
pub name: String,
|
||||
pub database: String,
|
||||
pub schema: Option<String>,
|
||||
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<SavedSqlFolder>,
|
||||
pub files: Vec<SavedSqlFile>,
|
||||
}
|
||||
|
|
@ -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<String>,
|
||||
name: String,
|
||||
database_name: String,
|
||||
schema_name: Option<String>,
|
||||
sql_text: String,
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
}
|
||||
|
||||
impl From<SavedSqlFolderRow> 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<SavedSqlFileRow> 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<SavedSqlLibrary, String> {
|
||||
let folder_rows: Vec<SavedSqlFolderRow> = 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<SavedSqlFileRow> = 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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<AppState>>) -> Result<SavedSqlLibrary, String> {
|
||||
state.storage.load_saved_sql_library().await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_saved_sql_folder(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
folder: SavedSqlFolder,
|
||||
) -> Result<SavedSqlFolder, String> {
|
||||
state.storage.save_saved_sql_folder(&folder).await?;
|
||||
Ok(folder)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_saved_sql_folder(state: State<'_, Arc<AppState>>, id: String) -> Result<(), String> {
|
||||
state.storage.delete_saved_sql_folder(&id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_saved_sql_file(state: State<'_, Arc<AppState>>, file: SavedSqlFile) -> Result<SavedSqlFile, String> {
|
||||
state.storage.save_saved_sql_file(&file).await?;
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_saved_sql_file(state: State<'_, Arc<AppState>>, id: String) -> Result<(), String> {
|
||||
state.storage.delete_saved_sql_file(&id).await
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<Arc<WebState>>) -> Result<Json<SavedSqlLibrary>, 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<Arc<WebState>>,
|
||||
Json(folder): Json<SavedSqlFolder>,
|
||||
) -> Result<Json<SavedSqlFolder>, 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<Arc<WebState>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<()>, 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<Arc<WebState>>,
|
||||
Json(file): Json<SavedSqlFile>,
|
||||
) -> Result<Json<SavedSqlFile>, 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<Arc<WebState>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<()>, AppError> {
|
||||
state.app.storage.delete_saved_sql_file(&id).await.map_err(AppError)?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
17
src/App.vue
17
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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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<ConnectionConfig[]> {
|
|||
return get("/api/connection/list");
|
||||
}
|
||||
|
||||
export async function loadSavedSqlLibrary(): Promise<SavedSqlLibrary> {
|
||||
return get("/api/saved-sql");
|
||||
}
|
||||
|
||||
export async function saveSavedSqlFolder(folder: SavedSqlFolder): Promise<SavedSqlFolder> {
|
||||
return post("/api/saved-sql/folders", folder);
|
||||
}
|
||||
|
||||
export async function deleteSavedSqlFolder(id: string): Promise<void> {
|
||||
return del(`/api/saved-sql/folders/${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
export async function saveSavedSqlFile(file: SavedSqlFile): Promise<SavedSqlFile> {
|
||||
return post("/api/saved-sql", file);
|
||||
}
|
||||
|
||||
export async function deleteSavedSqlFile(id: string): Promise<void> {
|
||||
return del(`/api/saved-sql/${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Schema
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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<ConnectionConfig[]> {
|
|||
return invoke("load_connections");
|
||||
}
|
||||
|
||||
export async function loadSavedSqlLibrary(): Promise<SavedSqlLibrary> {
|
||||
return invoke("load_saved_sql_library");
|
||||
}
|
||||
|
||||
export async function saveSavedSqlFolder(folder: SavedSqlFolder): Promise<SavedSqlFolder> {
|
||||
return invoke("save_saved_sql_folder", { folder });
|
||||
}
|
||||
|
||||
export async function deleteSavedSqlFolder(id: string): Promise<void> {
|
||||
return invoke("delete_saved_sql_folder", { id });
|
||||
}
|
||||
|
||||
export async function saveSavedSqlFile(file: SavedSqlFile): Promise<SavedSqlFile> {
|
||||
return invoke("save_saved_sql_file", { file });
|
||||
}
|
||||
|
||||
export async function deleteSavedSqlFile(id: string): Promise<void> {
|
||||
return invoke("delete_saved_sql_file", { id });
|
||||
}
|
||||
|
||||
export async function saveSidebarLayout(layout: import("@/types/database").SidebarLayout): Promise<void> {
|
||||
return invoke("save_sidebar_layout", { layout });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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<SavedSqlState>;
|
||||
return {
|
||||
|
|
@ -48,16 +30,36 @@ function loadState(): SavedSqlState {
|
|||
}
|
||||
|
||||
export const useSavedSqlStore = defineStore("savedSql", () => {
|
||||
const initial = loadState();
|
||||
const folders = ref<SavedSqlFolder[]>(initial.folders);
|
||||
const files = ref<SavedSqlFile[]>(initial.files);
|
||||
const folders = ref<SavedSqlFolder[]>([]);
|
||||
const files = ref<SavedSqlFile[]>([]);
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue