From 8efc3843ad8e758c6c1925bd31ebb8d4a42661af Mon Sep 17 00:00:00 2001 From: Simon Goller Date: Mon, 6 May 2024 15:15:47 +0200 Subject: [PATCH] Add sales-person REST service --- app/src/main.rs | 26 ++- dao_impl/src/lib.rs | 1 + dao_impl/src/sales_person.rs | 99 +++++++++++ .../20240506114107_add_sales_person.sql | 12 ++ rest/src/lib.rs | 7 + rest/src/sales_person.rs | 160 ++++++++++++++++++ 6 files changed, 302 insertions(+), 3 deletions(-) create mode 100644 dao_impl/src/sales_person.rs create mode 100644 migrations/20240506114107_add_sales_person.sql create mode 100644 rest/src/sales_person.rs diff --git a/app/src/main.rs b/app/src/main.rs index ce03020..18bb218 100644 --- a/app/src/main.rs +++ b/app/src/main.rs @@ -12,15 +12,23 @@ type SlotService = service_impl::slot::SlotServiceImpl< ClockService, UuidService, >; +type SalesPersonService = service_impl::sales_person::SalesPersonServiceImpl< + dao_impl::sales_person::SalesPersonDaoImpl, + PermissionService, + ClockService, + UuidService, +>; #[derive(Clone)] pub struct RestStateImpl { permission_service: Arc, slot_service: Arc, + sales_person_service: Arc, } impl rest::RestStateDef for RestStateImpl { type PermissionService = PermissionService; type SlotService = SlotService; + type SalesPersonService = SalesPersonService; fn permission_service(&self) -> Arc { self.permission_service.clone() @@ -28,11 +36,15 @@ impl rest::RestStateDef for RestStateImpl { fn slot_service(&self) -> Arc { self.slot_service.clone() } + fn sales_person_service(&self) -> Arc { + self.sales_person_service.clone() + } } impl RestStateImpl { pub fn new(pool: Arc>) -> Self { let permission_dao = dao_impl::PermissionDaoImpl::new(pool.clone()); - let slot_dao = dao_impl::slot::SlotDaoImpl::new(pool); + let slot_dao = dao_impl::slot::SlotDaoImpl::new(pool.clone()); + let sales_person_dao = dao_impl::sales_person::SalesPersonDaoImpl::new(pool); // Always authenticate with DEVUSER during development. // This is used to test the permission service locally without a login service. @@ -50,12 +62,20 @@ impl RestStateImpl { let slot_service = Arc::new(service_impl::slot::SlotServiceImpl::new( slot_dao.into(), permission_service.clone(), - clock_service, - uuid_service, + clock_service.clone(), + uuid_service.clone(), )); + let sales_person_service = + Arc::new(service_impl::sales_person::SalesPersonServiceImpl::new( + sales_person_dao.into(), + permission_service.clone(), + clock_service, + uuid_service, + )); Self { permission_service, slot_service, + sales_person_service, } } } diff --git a/dao_impl/src/lib.rs b/dao_impl/src/lib.rs index 273e935..bbb1bf9 100644 --- a/dao_impl/src/lib.rs +++ b/dao_impl/src/lib.rs @@ -4,6 +4,7 @@ use async_trait::async_trait; use dao::DaoError; use sqlx::{query, query_as, SqlitePool}; +pub mod sales_person; pub mod slot; pub trait ResultDbErrorExt { diff --git a/dao_impl/src/sales_person.rs b/dao_impl/src/sales_person.rs new file mode 100644 index 0000000..ad05482 --- /dev/null +++ b/dao_impl/src/sales_person.rs @@ -0,0 +1,99 @@ +use std::sync::Arc; + +use crate::ResultDbErrorExt; +use async_trait::async_trait; +use dao::{ + sales_person::{SalesPersonDao, SalesPersonEntity}, + DaoError, +}; +use sqlx::{query, query_as}; +use time::{format_description::well_known::Iso8601, PrimitiveDateTime}; +use uuid::Uuid; + +pub struct SalesPersonDaoImpl { + pub pool: Arc, +} +impl SalesPersonDaoImpl { + pub fn new(pool: Arc) -> Self { + Self { pool } + } +} + +struct SalesPersonDb { + id: Vec, + name: String, + inactive: bool, + deleted: Option, + update_version: Vec, +} +impl TryFrom<&SalesPersonDb> for SalesPersonEntity { + type Error = DaoError; + fn try_from(sales_person: &SalesPersonDb) -> Result { + Ok(Self { + id: Uuid::from_slice(sales_person.id.as_ref()).unwrap(), + name: sales_person.name.as_str().into(), + inactive: sales_person.inactive, + deleted: sales_person + .deleted + .as_ref() + .map(|deleted| PrimitiveDateTime::parse(deleted, &Iso8601::DATE)) + .transpose()?, + version: Uuid::from_slice(&sales_person.update_version).unwrap(), + }) + } +} + +#[async_trait] +impl SalesPersonDao for SalesPersonDaoImpl { + async fn all(&self) -> Result, DaoError> { + Ok(query_as!( + SalesPersonDb, + "SELECT id, name, inactive, deleted, update_version FROM sales_person WHERE deleted IS NULL" + ) + .fetch_all(self.pool.as_ref()) + .await + .map_db_error()? + .iter() + .map(SalesPersonEntity::try_from) + .collect::, DaoError>>()? + ) + } + async fn find_by_id(&self, id: Uuid) -> Result, DaoError> { + let id_vec = id.as_bytes().to_vec(); + Ok(query_as!( + SalesPersonDb, + "SELECT id, name, inactive, deleted, update_version FROM sales_person WHERE id = ?", + id_vec + ) + .fetch_optional(self.pool.as_ref()) + .await + .map_db_error()? + .as_ref() + .map(SalesPersonEntity::try_from) + .transpose()?) + } + async fn create(&self, entity: &SalesPersonEntity, process: &str) -> Result<(), DaoError> { + let id = entity.id.as_bytes().to_vec(); + let version = entity.version.as_bytes().to_vec(); + let name = entity.name.as_ref(); + let inactive = entity.inactive; + let deleted = entity.deleted.as_ref().map(|deleted| deleted.to_string()); + query!("INSERT INTO sales_person (id, name, inactive, deleted, update_version, update_process) VALUES (?, ?, ?, ?, ?, ?)", id, name, inactive, deleted, version, process) + .execute(self.pool.as_ref()) + .await + .map_db_error()?; + Ok(()) + } + async fn update(&self, entity: &SalesPersonEntity, process: &str) -> Result<(), DaoError> { + let id = entity.id.as_bytes().to_vec(); + let version = entity.version.as_bytes().to_vec(); + let name = entity.name.as_ref(); + let inactive = entity.inactive; + let deleted = entity.deleted.as_ref().map(|deleted| deleted.to_string()); + query!("UPDATE sales_person SET name = ?, inactive = ?, deleted = ?, update_version = ?, update_process = ? WHERE id = ?", name, inactive, deleted, version, process, id) + .execute(self.pool.as_ref()) + .await + .map_db_error()?; + Ok(()) + } +} diff --git a/migrations/20240506114107_add_sales_person.sql b/migrations/20240506114107_add_sales_person.sql new file mode 100644 index 0000000..342fd70 --- /dev/null +++ b/migrations/20240506114107_add_sales_person.sql @@ -0,0 +1,12 @@ +-- Add migration script here + +CREATE TABLE sales_person ( + id blob(16) NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + inactive BOOLEAN NOT NULL, + deleted TEXT, + + update_timestamp TEXT, + update_process TEXT NOT NULL, + update_version blob(16) NOT NULL +); \ No newline at end of file diff --git a/rest/src/lib.rs b/rest/src/lib.rs index 0fd6764..5f24017 100644 --- a/rest/src/lib.rs +++ b/rest/src/lib.rs @@ -1,6 +1,7 @@ use std::{convert::Infallible, sync::Arc}; mod permission; +mod sales_person; mod slot; use axum::{body::Body, response::Response, Router}; @@ -129,15 +130,21 @@ fn error_handler(result: Result) -> Response { pub trait RestStateDef: Clone + Send + Sync + 'static { type PermissionService: service::PermissionService + Send + Sync + 'static; type SlotService: service::slot::SlotService + Send + Sync + 'static; + type SalesPersonService: service::sales_person::SalesPersonService + + Send + + Sync + + 'static; fn permission_service(&self) -> Arc; fn slot_service(&self) -> Arc; + fn sales_person_service(&self) -> Arc; } pub async fn start_server(rest_state: RestState) { let app = Router::new() .nest("/permission", permission::generate_route()) .nest("/slot", slot::generate_route()) + .nest("/sales-person", sales_person::generate_route()) .with_state(rest_state); let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") .await diff --git a/rest/src/sales_person.rs b/rest/src/sales_person.rs new file mode 100644 index 0000000..392c769 --- /dev/null +++ b/rest/src/sales_person.rs @@ -0,0 +1,160 @@ +use std::sync::Arc; + +use axum::body::Body; +use axum::extract::Path; +use axum::routing::{delete, get, post, put}; +use axum::{extract::State, response::Response}; +use axum::{Json, Router}; +use serde::{Deserialize, Serialize}; +use service::sales_person::SalesPerson; +use service::sales_person::SalesPersonService; +use uuid::Uuid; + +use crate::{error_handler, RestError, RestStateDef}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct SalesPersonTO { + #[serde(default)] + pub id: Uuid, + pub name: Arc, + #[serde(default)] + pub inactive: bool, + #[serde(default)] + pub deleted: Option, + #[serde(rename = "$version")] + #[serde(default)] + pub version: Uuid, +} +impl From<&SalesPerson> for SalesPersonTO { + fn from(sales_person: &SalesPerson) -> Self { + Self { + id: sales_person.id, + name: sales_person.name.clone(), + inactive: sales_person.inactive, + deleted: sales_person.deleted, + version: sales_person.version, + } + } +} +impl From<&SalesPersonTO> for SalesPerson { + fn from(sales_person: &SalesPersonTO) -> Self { + Self { + id: sales_person.id, + name: sales_person.name.clone(), + inactive: sales_person.inactive, + deleted: sales_person.deleted, + version: sales_person.version, + } + } +} + +pub fn generate_route() -> Router { + Router::new() + .route("/", get(get_all_sales_persons::)) + .route("/:id", get(get_sales_person::)) + .route("/", post(create_sales_person::)) + .route("/:id", put(update_sales_person::)) + .route("/:id", delete(delete_sales_person::)) +} + +pub async fn get_all_sales_persons( + rest_state: State, +) -> Response { + error_handler( + (async { + let sales_persons: Arc<[SalesPersonTO]> = rest_state + .sales_person_service() + .get_all(()) + .await? + .iter() + .map(SalesPersonTO::from) + .collect(); + Ok(Response::builder() + .status(200) + .body(Body::new(serde_json::to_string(&sales_persons).unwrap())) + .unwrap()) + }) + .await, + ) +} + +pub async fn get_sales_person( + rest_state: State, + Path(sales_person_id): Path, +) -> Response { + error_handler( + (async { + let sales_person = SalesPersonTO::from( + &rest_state + .sales_person_service() + .get(sales_person_id, ()) + .await?, + ); + Ok(Response::builder() + .status(200) + .body(Body::new(serde_json::to_string(&sales_person).unwrap())) + .unwrap()) + }) + .await, + ) +} + +pub async fn create_sales_person( + rest_state: State, + Json(sales_person): Json, +) -> Response { + error_handler( + (async { + let sales_person = SalesPersonTO::from( + &rest_state + .sales_person_service() + .create(&(&sales_person).into(), ()) + .await?, + ); + Ok(Response::builder() + .status(200) + .body(Body::new(serde_json::to_string(&sales_person).unwrap())) + .unwrap()) + }) + .await, + ) +} + +pub async fn update_sales_person( + rest_state: State, + Path(sales_person_id): Path, + Json(sales_person): Json, +) -> Response { + error_handler( + (async { + if sales_person_id != sales_person.id { + return Err(RestError::InconsistentId(sales_person_id, sales_person.id)); + } + rest_state + .sales_person_service() + .update(&(&sales_person).into(), ()) + .await?; + Ok(Response::builder() + .status(200) + .body(Body::new(serde_json::to_string(&sales_person).unwrap())) + .unwrap()) + }) + .await, + ) +} + +pub async fn delete_sales_person( + rest_state: State, + Path(sales_person_id): Path, +) -> Response { + error_handler( + (async { + rest_state + .sales_person_service() + .delete(sales_person_id, ()) + .await?; + Ok(Response::builder().status(204).body(Body::empty()).unwrap()) + }) + .await, + ) +}