diff --git a/migrations/20220203034730_create-apps-table.sql b/migrations/20220203034730_create-apps-table.sql index 28e37b8..f8b3ae7 100644 --- a/migrations/20220203034730_create-apps-table.sql +++ b/migrations/20220203034730_create-apps-table.sql @@ -11,13 +11,14 @@ CREATE TABLE application ( CREATE TABLE application_category ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, category_name TEXT NOT NULL, + glyph TEXT, active BOOLEAN NOT NULL DEFAULT 1 ); CREATE TABLE bookmark ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - category_name TEXT NOT NULL, + bookmark_name TEXT NOT NULL, + url TEXT NOT NULL, active BOOLEAN NOT NULL DEFAULT 1, - glyph TEXT, bookmark_category_id INTEGER ); CREATE TABLE bookmark_category ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, diff --git a/src/api/application_category.rs b/src/api/application_categories.rs similarity index 99% rename from src/api/application_category.rs rename to src/api/application_categories.rs index 4be546b..37830f3 100644 --- a/src/api/application_category.rs +++ b/src/api/application_categories.rs @@ -20,6 +20,7 @@ pub async fn new_application_category( ) -> Result { let model = application_category::ActiveModel { id: NotSet, + glyph: Set(data.0.glyph), category_name: Set(data.0.category_name), active: Set(data.0.active), }; @@ -58,6 +59,7 @@ pub async fn update_application_category( let data = data.into_inner(); let ret = application_category::ActiveModel { id: Set(id), + glyph: Set(data.glyph), active: Set(data.active), category_name: Set(data.category_name), }; @@ -177,6 +179,7 @@ mod tests { let model = application_category::Model { id: 0, category_name: "Some name".into(), + glyph: None, active: true, }; diff --git a/src/api/bookmark_categories.rs b/src/api/bookmark_categories.rs new file mode 100644 index 0000000..fd5dbc1 --- /dev/null +++ b/src/api/bookmark_categories.rs @@ -0,0 +1,308 @@ +use tracing::instrument; + +use crate::api::api_prelude::*; +use crate::error::{Error, Result}; + +#[instrument] +#[get("")] +pub async fn list_bookmark_categories(state: web::Data) -> Result { + let cats: Vec = + BookmarkCategory::find().all(&state.db).await.unwrap(); + let count = cats.len(); + Ok(HttpResponse::Ok().json(ListObjects::new(cats, count))) +} + +#[instrument] +#[post("")] +pub async fn new_bookmark_category( + state: web::Data, + data: web::Json, +) -> Result { + let model = bookmark_category::ActiveModel { + id: NotSet, + category_name: Set(data.0.category_name), + glyph: Set(data.0.glyph), + active: Set(data.0.active), + }; + let rec = model.insert(&state.db).await?; + Ok(HttpResponse::Ok().json(rec)) +} + +#[instrument] +#[get("{id}")] +pub async fn get_bookmark_category( + state: web::Data, + id: web::Path, +) -> Result { + let id = id.into_inner(); + let res: Option = + BookmarkCategory::find_by_id(id).one(&state.db).await?; + match res { + Some(rec) => Ok(HttpResponse::Ok().json(rec)), + None => Err(Error::not_found()), + } +} + +#[instrument] +#[put("{id}")] +pub async fn update_bookmark_category( + state: web::Data, + + data: web::Json, + id: web::Path, +) -> Result { + let id = id.into_inner(); + let res: Option = + BookmarkCategory::find_by_id(id).one(&state.db).await?; + match res { + Some(_rec) => { + let data = data.into_inner(); + let ret = bookmark_category::ActiveModel { + id: Set(id), + active: Set(data.active), + glyph: Set(data.glyph), + category_name: Set(data.category_name), + }; + let model = ret.update(&state.db).await?; + Ok(HttpResponse::Ok().json(model)) + } + None => Err(Error::not_found()), + } +} + +#[instrument] +#[delete("{id}")] +pub async fn delete_bookmark_category( + state: web::Data, + id: web::Path, +) -> Result { + BookmarkCategory::delete_many() + .filter(bookmark_category::Column::Id.eq(id.into_inner())) + .exec(&state.db) + .await?; + + Ok(HttpResponse::Ok().body("")) +} + +#[instrument] +#[get("{id}/bookmarks")] +pub async fn bookmark_category_bookmarks( + state: web::Data, + id: web::Path, +) -> Result { + let recs: Vec = Bookmark::find() + .filter(bookmark::Column::BookmarkCategoryId.eq(id.into_inner())) + .all(&state.db) + .await + .unwrap(); + + let count = recs.len(); + Ok(HttpResponse::Ok().json(ListObjects::new(recs, count))) +} + +/// Routes for the bookmark endpoints. This binds up a scope with all endpoints for bookmarks, to make it easier to add them to the server. +pub fn routes() -> Scope { + web::scope("/bookmark_categories") + .service(bookmark_category_bookmarks) + .service(list_bookmark_categories) + .service(update_bookmark_category) + .service(delete_bookmark_category) + .service(new_bookmark_category) + .service(get_bookmark_category) +} + +#[cfg(test)] +mod tests { + + use crate::api::test_prelude::*; + use actix_web::http::Method; + + #[actix_rt::test] + async fn test_list_bookmark_categories() -> Result<()> { + let state = setup_state().await?; + bookmark_category::ActiveModel { + category_name: Set("Bookmark 1".into()), + active: Set(true), + ..Default::default() + } + .insert(&state.db) + .await?; + bookmark_category::ActiveModel { + category_name: Set("Bookmark 2".into()), + active: Set(true), + ..Default::default() + } + .insert(&state.db) + .await?; + + let req = actix_web::test::TestRequest::with_uri("/bookmark_categories") + .method(Method::GET) + .to_request(); + let resp = call_endpoint!(req, state); + assert_eq!(resp.status(), 200); + let data = get_response!(resp, ListObjects); + assert_eq!(2_usize, data.items.len()); + Ok(()) + } + + #[actix_rt::test] + async fn test_get_bookmark_categories() -> Result<()> { + let state = setup_state().await?; + let model = bookmark_category::ActiveModel { + category_name: Set("Bookmark 1".into()), + active: Set(true), + ..Default::default() + } + .insert(&state.db) + .await?; + + let req = + actix_web::test::TestRequest::with_uri(&format!("/bookmark_categories/{}", model.id)) + .method(Method::GET) + .to_request(); + let resp = call_endpoint!(req, state); + let status = resp.status(); + let mut data = get_response!(resp, crate::entity::bookmark_category::Model); + data.id = model.id; + assert_eq!(model, data); + assert_eq!(status, 200); + Ok(()) + } + + #[actix_rt::test] + async fn test_new_bookmark_category() -> Result<()> { + let model = bookmark_category::Model { + id: 0, + category_name: "Some name".into(), + glyph: None, + active: true, + }; + + let state = setup_state().await?; + + let req = actix_web::test::TestRequest::with_uri("/bookmark_categories") + .method(Method::POST) + .set_json(model.clone()) + .to_request(); + + let resp = call_endpoint!(req, state); + assert_eq!(resp.status(), 200); + let data = get_response!(resp, crate::entity::bookmark_category::Model); + assert_eq!(model, data); + assert_eq!(bookmark_category::Entity::find().count(&state.db).await?, 1); + Ok(()) + } + + #[actix_rt::test] + async fn test_update_bookmark_category() -> Result<()> { + let state = setup_state().await?; + bookmark_category::ActiveModel { + category_name: Set("Some name".into()), + active: Set(true), + ..Default::default() + } + .insert(&state.db) + .await?; + + let mut model = bookmark_category::ActiveModel { + category_name: Set("Some name".into()), + active: Set(true), + ..Default::default() + } + .insert(&state.db) + .await?; + + model.category_name = "Another name".into(); + + let req = + actix_web::test::TestRequest::with_uri(&format!("/bookmark_categories/{}", model.id)) + .method(Method::PUT) + .set_json(model.clone()) + .to_request(); + let resp = call_endpoint!(req, state); + assert_eq!(resp.status(), 200); + let mut data = get_response!(resp, crate::entity::bookmark_category::Model); + data.id = model.id; + assert_eq!(model, data, "Check API"); + + let db_model = bookmark_category::Entity::find_by_id(model.id) + .one(&state.db) + .await? + .unwrap(); + + assert_eq!(db_model, model, "Check DB"); + + Ok(()) + } + + #[actix_rt::test] + async fn test_delete_bookmark_category() -> Result<()> { + let state = setup_state().await?; + let model = bookmark_category::ActiveModel { + category_name: Set("Some name".into()), + active: Set(true), + ..Default::default() + } + .insert(&state.db) + .await?; + + let req = + actix_web::test::TestRequest::with_uri(&format!("/bookmark_categories/{}", model.id)) + .method(Method::DELETE) + .to_request(); + let resp = call_endpoint!(req, state); + assert_eq!(resp.status(), 200); + assert_eq!(bookmark_category::Entity::find().count(&state.db).await?, 0); + Ok(()) + } + + #[actix_rt::test] + async fn test_bookmark_categories_bookmarks() -> Result<()> { + let state = setup_state().await?; + let category = bookmark_category::ActiveModel { + category_name: Set("Bookmark 1".into()), + active: Set(true), + ..Default::default() + } + .insert(&state.db) + .await?; + bookmark::ActiveModel { + bookmark_name: Set("Bookmark 1".into()), + url: Set("http://somewhere/".into()), + active: Set(true), + bookmark_category_id: Set(Some(category.id)), + ..Default::default() + } + .insert(&state.db) + .await?; + bookmark::ActiveModel { + bookmark_name: Set("Bookmark 2".into()), + url: Set("http://somewhere/".into()), + bookmark_category_id: Set(Some(category.id)), + active: Set(true), + ..Default::default() + } + .insert(&state.db) + .await?; + bookmark::ActiveModel { + bookmark_name: Set("Bookmark 2".into()), + url: Set("http://somewhere/".into()), + active: Set(true), + ..Default::default() + } + .insert(&state.db) + .await?; + + let req = actix_web::test::TestRequest::with_uri(&format!( + "/bookmark_categories/{}/bookmarks", + category.id + )) + .method(Method::GET) + .to_request(); + let resp = call_endpoint!(req, state); + assert_eq!(resp.status(), 200); + let data = get_response!(resp, ListObjects); + assert_eq!(2_usize, data.items.len()); + Ok(()) + } +} diff --git a/src/api/bookmarks.rs b/src/api/bookmarks.rs new file mode 100644 index 0000000..801b485 --- /dev/null +++ b/src/api/bookmarks.rs @@ -0,0 +1,241 @@ +use tracing::instrument; + +use crate::api::api_prelude::*; +use crate::error::{Error, Result}; + +#[instrument] +#[get("")] +pub async fn list_bookmarks(state: web::Data) -> Result { + let apps: Vec = Bookmark::find().all(&state.db).await.unwrap(); + let count = apps.len(); + Ok(HttpResponse::Ok().json(ListObjects::new(apps, count))) +} + +#[instrument] +#[post("")] +pub async fn new_bookmark( + state: web::Data, + data: web::Json, +) -> Result { + let model = bookmark::ActiveModel { + id: NotSet, + bookmark_name: Set(data.0.bookmark_name), + url: Set(data.0.url), + active: Set(data.0.active), + bookmark_category_id: Set(data.0.bookmark_category_id), + }; + let app = model.insert(&state.db).await?; + Ok(HttpResponse::Ok().json(app)) +} + +#[instrument] +#[get("{id}")] +pub async fn get_bookmarks(state: web::Data, id: web::Path) -> Result { + let id = id.into_inner(); + let res: Option = Bookmark::find_by_id(id).one(&state.db).await?; + match res { + Some(app) => Ok(HttpResponse::Ok().json(app)), + None => Err(Error::not_found()), + } +} + +#[instrument] +#[put("{id}")] +pub async fn update_bookmarks( + state: web::Data, + + data: web::Json, + id: web::Path, +) -> Result { + let id = id.into_inner(); + let res: Option = Bookmark::find_by_id(id).one(&state.db).await?; + match res { + Some(_app) => { + let data = data.into_inner(); + let ret = bookmark::ActiveModel { + id: Set(id), + active: Set(data.active), + bookmark_name: Set(data.bookmark_name), + url: Set(data.url), + bookmark_category_id: Set(data.bookmark_category_id), + }; + let model = ret.update(&state.db).await?; + Ok(HttpResponse::Ok().json(model)) + } + None => Err(Error::not_found()), + } +} + +#[instrument] +#[delete("{id}")] +pub async fn delete_bookmark( + state: web::Data, + id: web::Path, +) -> Result { + Bookmark::delete_many() + .filter(bookmark::Column::Id.eq(id.into_inner())) + .exec(&state.db) + .await?; + + Ok(HttpResponse::Ok().body("")) +} + +/// Routes for the bookmark endpoints. This binds up a scope with all endpoints for bookmarks, to make it easier to add them to the server. +pub fn routes() -> Scope { + web::scope("/bookmarks") + .service(list_bookmarks) + .service(update_bookmarks) + .service(delete_bookmark) + .service(new_bookmark) + .service(get_bookmarks) +} + +#[cfg(test)] +mod tests { + + use crate::api::test_prelude::*; + use actix_web::http::Method; + + #[actix_rt::test] + async fn test_list_bookmarks() -> Result<()> { + let state = setup_state().await?; + bookmark::ActiveModel { + bookmark_name: Set("Bookmark 1".into()), + url: Set("http://somewhere/".into()), + active: Set(true), + ..Default::default() + } + .insert(&state.db) + .await?; + bookmark::ActiveModel { + bookmark_name: Set("Bookmark 2".into()), + url: Set("http://somewhere/".into()), + active: Set(true), + ..Default::default() + } + .insert(&state.db) + .await?; + + let req = actix_web::test::TestRequest::with_uri("/bookmarks") + .method(Method::GET) + .to_request(); + let resp = call_endpoint!(req, state); + assert_eq!(resp.status(), 200); + let data = get_response!(resp, ListObjects); + assert_eq!(2_usize, data.items.len()); + Ok(()) + } + + #[actix_rt::test] + async fn test_get_bookmarks() -> Result<()> { + let state = setup_state().await?; + let model = bookmark::ActiveModel { + bookmark_name: Set("Bookmark 1".into()), + url: Set("http://somewhere/".into()), + active: Set(true), + ..Default::default() + } + .insert(&state.db) + .await?; + + let req = actix_web::test::TestRequest::with_uri(&format!("/bookmarks/{}", model.id)) + .method(Method::GET) + .to_request(); + let resp = call_endpoint!(req, state); + let status = resp.status(); + let mut data = get_response!(resp, crate::entity::bookmark::Model); + data.id = model.id; + assert_eq!(model, data); + assert_eq!(status, 200); + Ok(()) + } + + #[actix_rt::test] + async fn test_new_bookmark() -> Result<()> { + let model = bookmark::Model { + id: 0, + bookmark_name: "Bookmark 1".into(), + url: "http://example.com".into(), + active: true, + bookmark_category_id: None, + }; + + let state = setup_state().await?; + + let req = actix_web::test::TestRequest::with_uri("/bookmarks") + .method(Method::POST) + .set_json(model.clone()) + .to_request(); + + let resp = call_endpoint!(req, state); + assert_eq!(resp.status(), 200); + let data = get_response!(resp, crate::entity::bookmark::Model); + assert_eq!(model, data); + assert_eq!(bookmark::Entity::find().count(&state.db).await?, 1); + Ok(()) + } + + #[actix_rt::test] + async fn test_update_bookmark() -> Result<()> { + let state = setup_state().await?; + bookmark::ActiveModel { + bookmark_name: Set("Bookmark 1".into()), + url: Set("http://somewhere/".into()), + active: Set(true), + ..Default::default() + } + .insert(&state.db) + .await?; + + let mut model = bookmark::ActiveModel { + bookmark_name: Set("Bookmark 2".into()), + url: Set("http://somewhere/".into()), + active: Set(true), + ..Default::default() + } + .insert(&state.db) + .await?; + + model.url = "http://updated.com".into(); + + let req = actix_web::test::TestRequest::with_uri(&format!("/bookmarks/{}", model.id)) + .method(Method::PUT) + .set_json(model.clone()) + .to_request(); + let resp = call_endpoint!(req, state); + assert_eq!(resp.status(), 200); + let mut data = get_response!(resp, crate::entity::bookmark::Model); + data.id = model.id; + assert_eq!(model, data, "Check API"); + + let db_model = bookmark::Entity::find_by_id(model.id) + .one(&state.db) + .await? + .unwrap(); + + assert_eq!(db_model, model, "Check DB"); + + Ok(()) + } + + #[actix_rt::test] + async fn test_delete_bookmark() -> Result<()> { + let state = setup_state().await?; + let model = bookmark::ActiveModel { + bookmark_name: Set("Bookmark 1".into()), + url: Set("http://somewhere/".into()), + active: Set(true), + ..Default::default() + } + .insert(&state.db) + .await?; + + let req = actix_web::test::TestRequest::with_uri(&format!("/bookmarks/{}", model.id)) + .method(Method::DELETE) + .to_request(); + let resp = call_endpoint!(req, state); + assert_eq!(resp.status(), 200); + assert_eq!(bookmark::Entity::find().count(&state.db).await?, 0); + Ok(()) + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 5e2f9a9..9d23104 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -21,7 +21,9 @@ macro_rules! call_endpoint { .wrap(tracing_actix_web::TracingLogger::default()) .app_data($state.clone()) .service(crate::api::applications::routes()) - .service(crate::api::application_category::routes()); + .service(crate::api::application_categories::routes()) + .service(crate::api::bookmarks::routes()) + .service(crate::api::bookmark_categories::routes()); let app = actix_web::test::init_service(a).await; let resp = actix_web::test::call_service(&app, $req).await; resp @@ -36,8 +38,10 @@ macro_rules! get_response { }}; } -pub mod application_category; +pub mod application_categories; pub mod applications; +pub mod bookmark_categories; +pub mod bookmarks; mod api_prelude { pub use super::ListObjects; @@ -78,7 +82,11 @@ mod test_prelude { let stmt: TableCreateStatement = schema.create_table_from_entity(application_category::Entity); db.execute(db.get_database_backend().build(&stmt)).await?; + let stmt: TableCreateStatement = schema.create_table_from_entity(bookmark::Entity); + db.execute(db.get_database_backend().build(&stmt)).await?; + let stmt: TableCreateStatement = schema.create_table_from_entity(bookmark_category::Entity); + db.execute(db.get_database_backend().build(&stmt)).await?; Ok(actix_web::web::Data::new(AppState { db })) } } diff --git a/src/entity/application_category.rs b/src/entity/application_category.rs index a009d05..10294a0 100644 --- a/src/entity/application_category.rs +++ b/src/entity/application_category.rs @@ -12,6 +12,7 @@ pub struct Model { pub id: i32, pub category_name: String, pub active: bool, + pub glyph: Option, } #[derive(Copy, Clone, Debug, EnumIter)] diff --git a/src/entity/bookmark.rs b/src/entity/bookmark.rs index c2f1238..bd5676e 100644 --- a/src/entity/bookmark.rs +++ b/src/entity/bookmark.rs @@ -7,10 +7,11 @@ use serde::{Deserialize, Serialize}; #[sea_orm(table_name = "bookmark")] pub struct Model { #[sea_orm(primary_key)] + #[serde(skip_deserializing)] pub id: i32, - pub category_name: String, + pub bookmark_name: String, + pub url: String, pub active: bool, - pub glyph: Option, pub bookmark_category_id: Option, } diff --git a/src/entity/bookmark_category.rs b/src/entity/bookmark_category.rs index e23d2be..12c2f0c 100644 --- a/src/entity/bookmark_category.rs +++ b/src/entity/bookmark_category.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; #[sea_orm(table_name = "bookmark_category")] pub struct Model { #[sea_orm(primary_key)] + #[serde(skip_deserializing)] pub id: i32, pub category_name: String, pub active: bool, diff --git a/src/main.rs b/src/main.rs index bc8221a..3eab936 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,7 +31,9 @@ async fn main() { App::new() .app_data(state.clone()) .service(api::applications::routes()) - .service(api::application_category::routes()) + .service(api::application_categories::routes()) + .service(api::bookmarks::routes()) + .service(api::bookmark_categories::routes()) }) .bind("127.0.0.1:8080") .unwrap()