Initial Commit
4926
src-tauri/Cargo.lock
generated
Normal file
23
src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "api-tester"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
repository = ""
|
||||
default-run = "api-tester"
|
||||
edition = "2021"
|
||||
rust-version = "1.60"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "2.0", features = [] }
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
tokio = { version = "1.36", features = ["full"] }
|
||||
uuid = { version = "1.7", features = ["v4", "serde"] }
|
||||
rusqlite = "0.32"
|
||||
|
||||
3
src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
9
src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default"
|
||||
]
|
||||
}
|
||||
1
src-tauri/gen/schemas/acl-manifests.json
Normal file
1
src-tauri/gen/schemas/capabilities.json
Normal file
@@ -0,0 +1 @@
|
||||
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default"]}}
|
||||
1756
src-tauri/gen/schemas/desktop-schema.json
Normal file
1756
src-tauri/gen/schemas/macOS-schema.json
Normal file
BIN
src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 974 B |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 903 B |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
75
src-tauri/src/api.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use crate::models::{RequestBody, ResponseBody};
|
||||
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use std::time::Instant;
|
||||
|
||||
pub async fn send_request(request: RequestBody) -> Result<ResponseBody, String> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Create headers
|
||||
let mut headers = HeaderMap::new();
|
||||
for header in request.headers.iter().filter(|h| h.enabled) {
|
||||
headers.insert(
|
||||
HeaderName::from_str(&header.key).map_err(|e| e.to_string())?,
|
||||
HeaderValue::from_str(&header.value).map_err(|e| e.to_string())?,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time = Instant::now();
|
||||
|
||||
// Build and send request
|
||||
let response = match request.method.as_str() {
|
||||
"GET" => client.get(&request.url),
|
||||
"POST" => client.post(&request.url),
|
||||
"PUT" => client.put(&request.url),
|
||||
"DELETE" => client.delete(&request.url),
|
||||
"PATCH" => client.patch(&request.url),
|
||||
_ => return Err("Unsupported HTTP method".to_string()),
|
||||
}
|
||||
.headers(headers);
|
||||
|
||||
// Add body for methods that support it
|
||||
let response = if let Some(body) = request.body {
|
||||
response.body(body)
|
||||
} else {
|
||||
response
|
||||
};
|
||||
|
||||
// Send the request
|
||||
let response = response
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to send request: {}", e))?;
|
||||
|
||||
// Process response
|
||||
let status = response.status().as_u16();
|
||||
let status_text = response.status().to_string();
|
||||
|
||||
// Convert response headers
|
||||
let headers: HashMap<String, String> = response
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k.to_string(),
|
||||
v.to_str().unwrap_or("").to_string(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let body = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read response body: {}", e))?;
|
||||
|
||||
let elapsed = start_time.elapsed().as_millis();
|
||||
|
||||
Ok(ResponseBody {
|
||||
status,
|
||||
status_text,
|
||||
headers,
|
||||
body,
|
||||
time: elapsed,
|
||||
})
|
||||
}
|
||||
49
src-tauri/src/commands/collection.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use crate::storage::{Storage, Collection};
|
||||
use std::sync::Mutex;
|
||||
use tauri::State;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_collection(
|
||||
state: State<'_, Mutex<Storage>>,
|
||||
workspace_id: String,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
) -> Result<Collection, String> {
|
||||
let mut storage = state.lock().map_err(|e| e.to_string())?;
|
||||
storage
|
||||
.create_collection(&workspace_id, &name, description.as_deref())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_workspace_collections(
|
||||
state: State<'_, Mutex<Storage>>,
|
||||
workspace_id: String,
|
||||
) -> Result<Vec<Collection>, String> {
|
||||
let mut storage = state.lock().map_err(|e| e.to_string())?;
|
||||
storage
|
||||
.get_workspace_collections(&workspace_id)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_collection(
|
||||
state: State<'_, Mutex<Storage>>,
|
||||
id: String,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
) -> Result<Collection, String> {
|
||||
let mut storage = state.lock().map_err(|e| e.to_string())?;
|
||||
storage
|
||||
.update_collection(&id, &name, description.as_deref())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_collection(
|
||||
state: State<'_, Mutex<Storage>>,
|
||||
id: String,
|
||||
) -> Result<(), String> {
|
||||
let mut storage = state.lock().map_err(|e| e.to_string())?;
|
||||
storage.delete_collection(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
5
src-tauri/src/commands/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod workspace;
|
||||
mod collection;
|
||||
|
||||
pub use workspace::*;
|
||||
pub use collection::*;
|
||||
54
src-tauri/src/commands/workspace.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use crate::storage::{Storage, Workspace};
|
||||
use std::sync::Mutex;
|
||||
use tauri::State;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_workspace(
|
||||
state: State<'_, Mutex<Storage>>,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
) -> Result<Workspace, String> {
|
||||
let mut storage = state.lock().map_err(|e| e.to_string())?;
|
||||
storage
|
||||
.create_workspace(&name, description.as_deref())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_workspaces(
|
||||
state: State<'_, Mutex<Storage>>,
|
||||
) -> Result<Vec<Workspace>, String> {
|
||||
let mut storage = state.lock().map_err(|e| e.to_string())?;
|
||||
storage.get_workspaces().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_workspace(
|
||||
state: State<'_, Mutex<Storage>>,
|
||||
id: String,
|
||||
) -> Result<Option<Workspace>, String> {
|
||||
let mut storage = state.lock().map_err(|e| e.to_string())?;
|
||||
storage.get_workspace(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_workspace(
|
||||
state: State<'_, Mutex<Storage>>,
|
||||
id: String,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
) -> Result<Workspace, String> {
|
||||
let mut storage = state.lock().map_err(|e| e.to_string())?;
|
||||
storage
|
||||
.update_workspace(&id, &name, description.as_deref())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_workspace(
|
||||
state: State<'_, Mutex<Storage>>,
|
||||
id: String,
|
||||
) -> Result<(), String> {
|
||||
let mut storage = state.lock().map_err(|e| e.to_string())?;
|
||||
storage.delete_workspace(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
13
src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
#[tauri::command]
|
||||
fn greet(name: &str) -> String {
|
||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![greet])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
35
src-tauri/src/main.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
mod api;
|
||||
mod models;
|
||||
mod storage;
|
||||
mod commands;
|
||||
|
||||
use storage::Storage;
|
||||
use tauri::Manager;
|
||||
use std::sync::Mutex;
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.setup(|app| {
|
||||
let app_dir = app.path().app_data_dir()
|
||||
.expect("Failed to get app data dir");
|
||||
|
||||
let storage = Storage::new(app_dir)
|
||||
.expect("Failed to initialize storage");
|
||||
|
||||
app.manage(Mutex::new(storage));
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::create_workspace,
|
||||
commands::get_workspaces,
|
||||
commands::get_workspace,
|
||||
commands::update_workspace,
|
||||
commands::delete_workspace,
|
||||
commands::create_collection,
|
||||
commands::get_workspace_collections,
|
||||
commands::update_collection,
|
||||
commands::delete_collection,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
35
src-tauri/src/models.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Variable {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Header {
|
||||
pub id: String,
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RequestBody {
|
||||
pub url: String,
|
||||
pub method: String,
|
||||
pub headers: Vec<Header>,
|
||||
pub body: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ResponseBody {
|
||||
pub status: u16,
|
||||
pub status_text: String,
|
||||
pub headers: HashMap<String, String>,
|
||||
pub body: String,
|
||||
pub time: u128,
|
||||
}
|
||||
103
src-tauri/src/storage/collection.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use super::*;
|
||||
use rusqlite::{params, Result};
|
||||
|
||||
impl Storage {
|
||||
pub fn create_collection(
|
||||
&mut self,
|
||||
workspace_id: &str,
|
||||
name: &str,
|
||||
description: Option<&str>,
|
||||
) -> Result<Collection> {
|
||||
let now = Self::now();
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
self.conn.get_mut().unwrap().execute(
|
||||
"INSERT INTO collections (id, workspace_id, name, description, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![id, workspace_id, name, description, now, now],
|
||||
)?;
|
||||
|
||||
Ok(Collection {
|
||||
id,
|
||||
workspace_id: workspace_id.to_string(),
|
||||
name: name.to_string(),
|
||||
description: description.map(String::from),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_workspace_collections(&mut self, workspace_id: &str) -> Result<Vec<Collection>> {
|
||||
let mut stmt = self.conn.get_mut().unwrap().prepare(
|
||||
"SELECT id, workspace_id, name, description, created_at, updated_at
|
||||
FROM collections
|
||||
WHERE workspace_id = ?
|
||||
ORDER BY created_at DESC"
|
||||
)?;
|
||||
|
||||
let collections = stmt.query_map([workspace_id], |row| {
|
||||
Ok(Collection {
|
||||
id: row.get(0)?,
|
||||
workspace_id: row.get(1)?,
|
||||
name: row.get(2)?,
|
||||
description: row.get(3)?,
|
||||
created_at: row.get(4)?,
|
||||
updated_at: row.get(5)?,
|
||||
})
|
||||
})?;
|
||||
|
||||
collections.collect()
|
||||
}
|
||||
|
||||
pub fn update_collection(
|
||||
&mut self,
|
||||
id: &str,
|
||||
name: &str,
|
||||
description: Option<&str>,
|
||||
) -> Result<Collection> {
|
||||
let now = Self::now();
|
||||
|
||||
self.conn.get_mut().unwrap().execute(
|
||||
"UPDATE collections
|
||||
SET name = ?1, description = ?2, updated_at = ?3
|
||||
WHERE id = ?4",
|
||||
params![name, description, now, id],
|
||||
)?;
|
||||
|
||||
let mut stmt = self.conn.get_mut().unwrap().prepare(
|
||||
"SELECT workspace_id, created_at FROM collections WHERE id = ?"
|
||||
)?;
|
||||
|
||||
let (workspace_id, created_at) = stmt.query_row([id], |row| {
|
||||
Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
|
||||
})?;
|
||||
|
||||
Ok(Collection {
|
||||
id: id.to_string(),
|
||||
workspace_id,
|
||||
name: name.to_string(),
|
||||
description: description.map(String::from),
|
||||
created_at,
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_collection(&mut self, id: &str) -> Result<()> {
|
||||
let tx = self.conn.get_mut().unwrap().transaction()?;
|
||||
|
||||
// Delete all requests in this collection
|
||||
tx.execute(
|
||||
"DELETE FROM requests WHERE collection_id = ?",
|
||||
params![id],
|
||||
)?;
|
||||
|
||||
// Delete the collection
|
||||
tx.execute(
|
||||
"DELETE FROM collections WHERE id = ?",
|
||||
params![id],
|
||||
)?;
|
||||
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
158
src-tauri/src/storage/mod.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
mod workspace;
|
||||
mod collection;
|
||||
|
||||
pub use workspace::*;
|
||||
pub use collection::*;
|
||||
|
||||
use rusqlite::Connection;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::sync::RwLock;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Workspace {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub created_at: i64,
|
||||
pub updated_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Collection {
|
||||
pub id: String,
|
||||
pub workspace_id: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub created_at: i64,
|
||||
pub updated_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Variable {
|
||||
pub id: String,
|
||||
pub workspace_id: String,
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
pub description: Option<String>,
|
||||
pub created_at: i64,
|
||||
pub updated_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Request {
|
||||
pub id: String,
|
||||
pub collection_id: String,
|
||||
pub name: String,
|
||||
pub method: String,
|
||||
pub url: String,
|
||||
pub created_at: i64,
|
||||
pub updated_at: i64,
|
||||
}
|
||||
|
||||
pub struct Storage {
|
||||
conn: RwLock<Connection>,
|
||||
data_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
pub fn new(app_dir: PathBuf) -> rusqlite::Result<Self> {
|
||||
let db_path = app_dir.join("api-client.db");
|
||||
let data_dir = app_dir.join("data");
|
||||
|
||||
fs::create_dir_all(&data_dir).map_err(|e| rusqlite::Error::InvalidPath(data_dir.clone()))?;
|
||||
|
||||
let conn = Connection::open(db_path)?;
|
||||
Self::init_database(&conn)?;
|
||||
|
||||
Ok(Storage { conn: RwLock::new(conn), data_dir })
|
||||
}
|
||||
|
||||
fn init_database(conn: &Connection) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS workspaces (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS collections (
|
||||
id TEXT PRIMARY KEY,
|
||||
workspace_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(id)
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS variables (
|
||||
id TEXT PRIMARY KEY,
|
||||
workspace_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
description TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(id)
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS requests (
|
||||
id TEXT PRIMARY KEY,
|
||||
collection_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
method TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(collection_id) REFERENCES collections(id)
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create indexes
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_collections_workspace
|
||||
ON collections(workspace_id)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_variables_workspace
|
||||
ON variables(workspace_id)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_requests_collection
|
||||
ON requests(collection_id)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn now() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64
|
||||
}
|
||||
|
||||
pub(crate) fn get_request_data_path(&self, request_id: &str) -> PathBuf {
|
||||
self.data_dir.join(format!("request_{}.json", request_id))
|
||||
}
|
||||
}
|
||||
115
src-tauri/src/storage/workspace.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use super::*;
|
||||
use rusqlite::{params, Result, OptionalExtension};
|
||||
|
||||
impl Storage {
|
||||
pub fn create_workspace(&mut self, name: &str, description: Option<&str>) -> Result<Workspace> {
|
||||
let now = Self::now();
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
self.conn.get_mut().unwrap().execute(
|
||||
"INSERT INTO workspaces (id, name, description, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
params![id, name, description, now, now],
|
||||
)?;
|
||||
|
||||
Ok(Workspace {
|
||||
id,
|
||||
name: name.to_string(),
|
||||
description: description.map(String::from),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_workspaces(&mut self) -> Result<Vec<Workspace>> {
|
||||
let mut stmt = self.conn.get_mut().unwrap().prepare(
|
||||
"SELECT id, name, description, created_at, updated_at
|
||||
FROM workspaces
|
||||
ORDER BY created_at DESC"
|
||||
)?;
|
||||
|
||||
let workspace_iter = stmt.query_map([], |row| {
|
||||
Ok(Workspace {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
description: row.get(2)?,
|
||||
created_at: row.get(3)?,
|
||||
updated_at: row.get(4)?,
|
||||
})
|
||||
})?;
|
||||
|
||||
workspace_iter.collect()
|
||||
}
|
||||
|
||||
pub fn get_workspace(&mut self, id: &str) -> Result<Option<Workspace>> {
|
||||
let mut stmt = self.conn.get_mut().unwrap().prepare(
|
||||
"SELECT id, name, description, created_at, updated_at
|
||||
FROM workspaces
|
||||
WHERE id = ?"
|
||||
)?;
|
||||
|
||||
stmt.query_row(params![id], |row| {
|
||||
Ok(Workspace {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
description: row.get(2)?,
|
||||
created_at: row.get(3)?,
|
||||
updated_at: row.get(4)?,
|
||||
})
|
||||
})
|
||||
.optional()
|
||||
}
|
||||
|
||||
pub fn update_workspace(&mut self, id: &str, name: &str, description: Option<&str>) -> Result<Workspace> {
|
||||
let now = Self::now();
|
||||
|
||||
self.conn.get_mut().unwrap().execute(
|
||||
"UPDATE workspaces
|
||||
SET name = ?1, description = ?2, updated_at = ?3
|
||||
WHERE id = ?4",
|
||||
params![name, description, now, id],
|
||||
)?;
|
||||
|
||||
Ok(Workspace {
|
||||
id: id.to_string(),
|
||||
name: name.to_string(),
|
||||
description: description.map(String::from),
|
||||
created_at: self.get_workspace(id)?.map(|w| w.created_at).unwrap_or(now),
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_workspace(&mut self, id: &str) -> Result<()> {
|
||||
// Start a transaction to ensure data consistency
|
||||
let tx = self.conn.get_mut().unwrap().transaction()?;
|
||||
|
||||
// Delete variables
|
||||
tx.execute(
|
||||
"DELETE FROM variables WHERE workspace_id = ?",
|
||||
params![id],
|
||||
)?;
|
||||
|
||||
// Delete requests from collections in this workspace
|
||||
tx.execute(
|
||||
"DELETE FROM requests
|
||||
WHERE collection_id IN (
|
||||
SELECT id FROM collections WHERE workspace_id = ?
|
||||
)",
|
||||
params![id],
|
||||
)?;
|
||||
|
||||
// Delete collections
|
||||
tx.execute(
|
||||
"DELETE FROM collections WHERE workspace_id = ?",
|
||||
params![id],
|
||||
)?;
|
||||
|
||||
// Finally delete the workspace
|
||||
tx.execute("DELETE FROM workspaces WHERE id = ?", params![id])?;
|
||||
|
||||
// Commit the transaction
|
||||
tx.commit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
35
src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "api-client",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.api-client.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "bun run dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "bun run build",
|
||||
"frontendDist": "../build"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "api-client",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||