diff --git a/agdb_server/openapi/schema.json b/agdb_server/openapi/schema.json index eddb96c89..f06c0520e 100644 --- a/agdb_server/openapi/schema.json +++ b/agdb_server/openapi/schema.json @@ -70,6 +70,32 @@ } } }, + "/delete_db": { + "post": { + "tags": [ + "crate::app" + ], + "operationId": "delete_db", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteServerDatabase" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Database deleted" + }, + "403": { + "description": "Database not found for user" + } + } + } + }, "/list": { "post": { "tags": [ @@ -140,6 +166,17 @@ "memory" ] }, + "DeleteServerDatabase": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + } + }, "ServerDatabase": { "type": "object", "required": [ diff --git a/agdb_server/src/api.rs b/agdb_server/src/api.rs index acfc43e6d..4a5a64ce7 100644 --- a/agdb_server/src/api.rs +++ b/agdb_server/src/api.rs @@ -5,12 +5,14 @@ use utoipa::OpenApi; paths( crate::app::create_db, crate::app::create_user, + crate::app::delete_db, crate::app::list, crate::app::login ), components(schemas( crate::app::ServerDatabase, crate::app::DbType, + crate::app::DeleteServerDatabase, crate::app::UserCredentials, crate::app::UserToken )) diff --git a/agdb_server/src/app.rs b/agdb_server/src/app.rs index 98a84a291..2cf87807f 100644 --- a/agdb_server/src/app.rs +++ b/agdb_server/src/app.rs @@ -9,6 +9,7 @@ use crate::logger; use crate::password::Password; use crate::utilities; use agdb::DbId; +use anyhow::anyhow; use axum::async_trait; use axum::body; use axum::extract::FromRef; @@ -53,6 +54,11 @@ pub(crate) struct ServerDatabase { pub(crate) db_type: DbType, } +#[derive(Deserialize, ToSchema)] +pub(crate) struct DeleteServerDatabase { + pub(crate) name: String, +} + #[derive(Deserialize, ToSchema)] pub(crate) struct UserCredentials { pub(crate) name: String, @@ -133,6 +139,7 @@ pub(crate) fn app(shutdown_sender: Sender<()>, db_pool: DbPool) -> Router { .route("/error", routing::get(test_error)) .route("/create_db", routing::post(create_db)) .route("/create_user", routing::post(create_user)) + .route("/delete_db", routing::post(delete_db)) .route("/list", routing::get(list)) .route("/login", routing::post(login)) .layer(logger) @@ -213,6 +220,31 @@ pub(crate) async fn create_user( Ok(StatusCode::CREATED) } +#[utoipa::path(post, + path = "/delete_db", + request_body = DeleteServerDatabase, + responses( + (status = 200, description = "Database deleted"), + (status = 403, description = "Database not found for user"), + ) +)] +pub(crate) async fn delete_db( + user: UserId, + State(db_pool): State, + Json(request): Json, +) -> Result { + let db = db_pool + .find_user_database(user.0, &request.name) + .map_err(|_| ServerError { + status: StatusCode::FORBIDDEN, + error: anyhow!("Database not found for user"), + })?; + + db_pool.delete_database(db)?; + + Ok(StatusCode::OK) +} + #[utoipa::path(post, path = "/list", responses( @@ -224,7 +256,7 @@ pub(crate) async fn list( State(db_pool): State, ) -> Result<(StatusCode, Json>), ServerError> { let dbs = db_pool - .find_databases(user.0)? + .find_user_databases(user.0)? .into_iter() .map(|db| db.into()) .collect(); diff --git a/agdb_server/src/db.rs b/agdb_server/src/db.rs index df2f5ec9d..18c8bfd83 100644 --- a/agdb_server/src/db.rs +++ b/agdb_server/src/db.rs @@ -11,6 +11,7 @@ use agdb::StorageSlice; use agdb::UserValue; use anyhow::anyhow; use std::collections::HashMap; +use std::path::Path; use std::sync::Arc; use std::sync::PoisonError; use std::sync::RwLock; @@ -106,7 +107,7 @@ impl StorageData for ServerDbStorage { } type ServerDbImpl = DbImpl; -pub(crate) struct ServerDb(Arc>>); +pub(crate) struct ServerDb(Arc>); #[allow(dead_code)] pub(crate) struct DbPoolImpl { @@ -184,6 +185,28 @@ impl DbPool { Ok(()) } + pub(crate) fn delete_database(&self, db: Database) -> anyhow::Result<()> { + self.0 + .server_db + .get_mut()? + .exec_mut(&QueryBuilder::remove().ids(db.db_id.unwrap()).query())?; + + let delete_db = self.get_pool_mut()?.remove(&db.name).unwrap(); + let filename = delete_db.get()?.filename().to_string(); + let path = Path::new(&filename); + + if path.exists() { + std::fs::remove_file(&filename)?; + let dot_file = path + .parent() + .unwrap_or(Path::new("./")) + .join(format!(".{filename}")); + std::fs::remove_file(dot_file)?; + } + + Ok(()) + } + pub(crate) fn find_database(&self, name: &str) -> anyhow::Result { Ok(self .0 @@ -207,7 +230,7 @@ impl DbPool { .try_into()?) } - pub(crate) fn find_databases(&self, user: DbId) -> anyhow::Result> { + pub(crate) fn find_user_databases(&self, user: DbId) -> anyhow::Result> { Ok(self .0 .server_db @@ -226,6 +249,28 @@ impl DbPool { .try_into()?) } + pub(crate) fn find_user_database(&self, user: DbId, name: &str) -> anyhow::Result { + Ok(self + .0 + .server_db + .get()? + .exec( + &QueryBuilder::select() + .ids( + QueryBuilder::search() + .from(user) + .where_() + .distance(agdb::CountComparison::Equal(2)) + .and() + .key("name") + .value(agdb::Comparison::Equal(name.into())) + .query(), + ) + .query(), + )? + .try_into()?) + } + pub(crate) fn find_user(&self, name: &str) -> anyhow::Result { Ok(self .0 diff --git a/agdb_server/tests/framework/mod.rs b/agdb_server/tests/framework/mod.rs index ed9dd96c2..5e7d07361 100644 --- a/agdb_server/tests/framework/mod.rs +++ b/agdb_server/tests/framework/mod.rs @@ -13,10 +13,10 @@ const DEFAULT_PORT: u16 = 3000; static PORT: AtomicU16 = AtomicU16::new(DEFAULT_PORT); pub struct TestServer { - dir: String, - port: u16, - process: Child, - client: Client, + pub dir: String, + pub port: u16, + pub process: Child, + pub client: Client, } impl TestServer { diff --git a/agdb_server/tests/server_test.rs b/agdb_server/tests/server_test.rs index e56ad468d..26d95e931 100644 --- a/agdb_server/tests/server_test.rs +++ b/agdb_server/tests/server_test.rs @@ -1,7 +1,7 @@ mod framework; use crate::framework::TestServer; -use std::collections::HashMap; +use std::{collections::HashMap, path::Path}; #[tokio::test] async fn config_port() -> anyhow::Result<()> { @@ -98,7 +98,7 @@ async fn list() -> anyhow::Result<()> { assert_eq!(status, 200); //ok assert_eq!(list, "[]"); - let mut dbs = Vec::with_capacity(2); + let mut dbs = Vec::with_capacity(3); let mut db = HashMap::new(); db.insert("name", "my_db"); db.insert("db_type", "memory"); @@ -122,5 +122,85 @@ async fn list() -> anyhow::Result<()> { assert_eq!(dbs, list); + assert!(!Path::new(&server.dir).join("my_db").exists()); + assert!(Path::new(&server.dir).join("my_db2").exists()); + assert!(Path::new(&server.dir).join("my_db3").exists()); + + Ok(()) +} + +#[tokio::test] +async fn delete_db() -> anyhow::Result<()> { + let server = TestServer::new()?; + let mut user = HashMap::new(); + user.insert("name", "alice"); + user.insert("password", "mypassword123"); + assert_eq!(server.post("/create_user", &user).await?, 201); //created + + let mut delete_db = HashMap::new(); + delete_db.insert("name", "my_db"); + + assert_eq!( + server.post_auth("/delete_db", "token", &delete_db).await?, + 401 + ); //unauthorized + + let mut dbs = Vec::with_capacity(2); + let mut db = HashMap::new(); + db.insert("name", "my_db"); + db.insert("db_type", "mapped"); + dbs.push(db.clone()); + db.insert("name", "my_db2"); + db.insert("db_type", "file"); + dbs.push(db); + + let (status, token) = server.post_response("/login", &user).await?; + assert_eq!(status, 200); //ok + + assert_eq!(server.post_auth("/create_db", &token, &dbs[0]).await?, 201); //created + assert_eq!(server.post_auth("/create_db", &token, &dbs[1]).await?, 201); //created + + assert!(Path::new(&server.dir).join("my_db").exists()); + assert!(Path::new(&server.dir).join("my_db2").exists()); + + assert_eq!( + server.post_auth("/delete_db", &token, &delete_db).await?, + 200 + ); + + assert!(!Path::new(&server.dir).join("my_db").exists()); + assert!(Path::new(&server.dir).join("my_db2").exists()); + + assert_eq!( + server.post_auth("/delete_db", &token, &delete_db).await?, + 403 + ); + + let (status, list) = server.get_auth_response("/list", &token).await?; + assert_eq!(status, 200); //ok + + let list: Vec> = serde_json::from_str(&list)?; + + assert_eq!(dbs[1], list[0]); + + dbs[0].insert("db_type", "memory"); + + assert_eq!(server.post_auth("/create_db", &token, &dbs[0]).await?, 201); //created + + assert!(!Path::new(&server.dir).join("my_db").exists()); + + user.insert("name", "bob"); + assert_eq!(server.post("/create_user", &user).await?, 201); //created + let (status, token2) = server.post_response("/login", &user).await?; + assert_eq!(status, 200); //ok + assert_eq!( + server.post_auth("/delete_db", &token2, &delete_db).await?, + 403 + ); + assert_eq!( + server.post_auth("/delete_db", &token, &delete_db).await?, + 200 + ); + Ok(()) }