Add sales-person REST service
This commit is contained in:
parent
ad88a1c983
commit
8efc3843ad
6 changed files with 302 additions and 3 deletions
|
|
@ -12,15 +12,23 @@ type SlotService = service_impl::slot::SlotServiceImpl<
|
||||||
ClockService,
|
ClockService,
|
||||||
UuidService,
|
UuidService,
|
||||||
>;
|
>;
|
||||||
|
type SalesPersonService = service_impl::sales_person::SalesPersonServiceImpl<
|
||||||
|
dao_impl::sales_person::SalesPersonDaoImpl,
|
||||||
|
PermissionService,
|
||||||
|
ClockService,
|
||||||
|
UuidService,
|
||||||
|
>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RestStateImpl {
|
pub struct RestStateImpl {
|
||||||
permission_service: Arc<PermissionService>,
|
permission_service: Arc<PermissionService>,
|
||||||
slot_service: Arc<SlotService>,
|
slot_service: Arc<SlotService>,
|
||||||
|
sales_person_service: Arc<SalesPersonService>,
|
||||||
}
|
}
|
||||||
impl rest::RestStateDef for RestStateImpl {
|
impl rest::RestStateDef for RestStateImpl {
|
||||||
type PermissionService = PermissionService;
|
type PermissionService = PermissionService;
|
||||||
type SlotService = SlotService;
|
type SlotService = SlotService;
|
||||||
|
type SalesPersonService = SalesPersonService;
|
||||||
|
|
||||||
fn permission_service(&self) -> Arc<Self::PermissionService> {
|
fn permission_service(&self) -> Arc<Self::PermissionService> {
|
||||||
self.permission_service.clone()
|
self.permission_service.clone()
|
||||||
|
|
@ -28,11 +36,15 @@ impl rest::RestStateDef for RestStateImpl {
|
||||||
fn slot_service(&self) -> Arc<Self::SlotService> {
|
fn slot_service(&self) -> Arc<Self::SlotService> {
|
||||||
self.slot_service.clone()
|
self.slot_service.clone()
|
||||||
}
|
}
|
||||||
|
fn sales_person_service(&self) -> Arc<Self::SalesPersonService> {
|
||||||
|
self.sales_person_service.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
impl RestStateImpl {
|
impl RestStateImpl {
|
||||||
pub fn new(pool: Arc<sqlx::Pool<sqlx::Sqlite>>) -> Self {
|
pub fn new(pool: Arc<sqlx::Pool<sqlx::Sqlite>>) -> Self {
|
||||||
let permission_dao = dao_impl::PermissionDaoImpl::new(pool.clone());
|
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.
|
// Always authenticate with DEVUSER during development.
|
||||||
// This is used to test the permission service locally without a login service.
|
// 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(
|
let slot_service = Arc::new(service_impl::slot::SlotServiceImpl::new(
|
||||||
slot_dao.into(),
|
slot_dao.into(),
|
||||||
permission_service.clone(),
|
permission_service.clone(),
|
||||||
|
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,
|
clock_service,
|
||||||
uuid_service,
|
uuid_service,
|
||||||
));
|
));
|
||||||
Self {
|
Self {
|
||||||
permission_service,
|
permission_service,
|
||||||
slot_service,
|
slot_service,
|
||||||
|
sales_person_service,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use async_trait::async_trait;
|
||||||
use dao::DaoError;
|
use dao::DaoError;
|
||||||
use sqlx::{query, query_as, SqlitePool};
|
use sqlx::{query, query_as, SqlitePool};
|
||||||
|
|
||||||
|
pub mod sales_person;
|
||||||
pub mod slot;
|
pub mod slot;
|
||||||
|
|
||||||
pub trait ResultDbErrorExt<T, E> {
|
pub trait ResultDbErrorExt<T, E> {
|
||||||
|
|
|
||||||
99
dao_impl/src/sales_person.rs
Normal file
99
dao_impl/src/sales_person.rs
Normal file
|
|
@ -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<sqlx::SqlitePool>,
|
||||||
|
}
|
||||||
|
impl SalesPersonDaoImpl {
|
||||||
|
pub fn new(pool: Arc<sqlx::SqlitePool>) -> Self {
|
||||||
|
Self { pool }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SalesPersonDb {
|
||||||
|
id: Vec<u8>,
|
||||||
|
name: String,
|
||||||
|
inactive: bool,
|
||||||
|
deleted: Option<String>,
|
||||||
|
update_version: Vec<u8>,
|
||||||
|
}
|
||||||
|
impl TryFrom<&SalesPersonDb> for SalesPersonEntity {
|
||||||
|
type Error = DaoError;
|
||||||
|
fn try_from(sales_person: &SalesPersonDb) -> Result<Self, Self::Error> {
|
||||||
|
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<Arc<[SalesPersonEntity]>, 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::<Result<Arc<[SalesPersonEntity]>, DaoError>>()?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> Result<Option<SalesPersonEntity>, 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
12
migrations/20240506114107_add_sales_person.sql
Normal file
12
migrations/20240506114107_add_sales_person.sql
Normal file
|
|
@ -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
|
||||||
|
);
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use std::{convert::Infallible, sync::Arc};
|
use std::{convert::Infallible, sync::Arc};
|
||||||
|
|
||||||
mod permission;
|
mod permission;
|
||||||
|
mod sales_person;
|
||||||
mod slot;
|
mod slot;
|
||||||
|
|
||||||
use axum::{body::Body, response::Response, Router};
|
use axum::{body::Body, response::Response, Router};
|
||||||
|
|
@ -129,15 +130,21 @@ fn error_handler(result: Result<Response, RestError>) -> Response {
|
||||||
pub trait RestStateDef: Clone + Send + Sync + 'static {
|
pub trait RestStateDef: Clone + Send + Sync + 'static {
|
||||||
type PermissionService: service::PermissionService<Context = Context> + Send + Sync + 'static;
|
type PermissionService: service::PermissionService<Context = Context> + Send + Sync + 'static;
|
||||||
type SlotService: service::slot::SlotService<Context = Context> + Send + Sync + 'static;
|
type SlotService: service::slot::SlotService<Context = Context> + Send + Sync + 'static;
|
||||||
|
type SalesPersonService: service::sales_person::SalesPersonService<Context = Context>
|
||||||
|
+ Send
|
||||||
|
+ Sync
|
||||||
|
+ 'static;
|
||||||
|
|
||||||
fn permission_service(&self) -> Arc<Self::PermissionService>;
|
fn permission_service(&self) -> Arc<Self::PermissionService>;
|
||||||
fn slot_service(&self) -> Arc<Self::SlotService>;
|
fn slot_service(&self) -> Arc<Self::SlotService>;
|
||||||
|
fn sales_person_service(&self) -> Arc<Self::SalesPersonService>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start_server<RestState: RestStateDef>(rest_state: RestState) {
|
pub async fn start_server<RestState: RestStateDef>(rest_state: RestState) {
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.nest("/permission", permission::generate_route())
|
.nest("/permission", permission::generate_route())
|
||||||
.nest("/slot", slot::generate_route())
|
.nest("/slot", slot::generate_route())
|
||||||
|
.nest("/sales-person", sales_person::generate_route())
|
||||||
.with_state(rest_state);
|
.with_state(rest_state);
|
||||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
160
rest/src/sales_person.rs
Normal file
160
rest/src/sales_person.rs
Normal file
|
|
@ -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<str>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub inactive: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub deleted: Option<time::PrimitiveDateTime>,
|
||||||
|
#[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<RestState: RestStateDef>() -> Router<RestState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(get_all_sales_persons::<RestState>))
|
||||||
|
.route("/:id", get(get_sales_person::<RestState>))
|
||||||
|
.route("/", post(create_sales_person::<RestState>))
|
||||||
|
.route("/:id", put(update_sales_person::<RestState>))
|
||||||
|
.route("/:id", delete(delete_sales_person::<RestState>))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_all_sales_persons<RestState: RestStateDef>(
|
||||||
|
rest_state: State<RestState>,
|
||||||
|
) -> 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<RestState: RestStateDef>(
|
||||||
|
rest_state: State<RestState>,
|
||||||
|
Path(sales_person_id): Path<Uuid>,
|
||||||
|
) -> 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<RestState: RestStateDef>(
|
||||||
|
rest_state: State<RestState>,
|
||||||
|
Json(sales_person): Json<SalesPersonTO>,
|
||||||
|
) -> 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<RestState: RestStateDef>(
|
||||||
|
rest_state: State<RestState>,
|
||||||
|
Path(sales_person_id): Path<Uuid>,
|
||||||
|
Json(sales_person): Json<SalesPersonTO>,
|
||||||
|
) -> 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<RestState: RestStateDef>(
|
||||||
|
rest_state: State<RestState>,
|
||||||
|
Path(sales_person_id): Path<Uuid>,
|
||||||
|
) -> Response {
|
||||||
|
error_handler(
|
||||||
|
(async {
|
||||||
|
rest_state
|
||||||
|
.sales_person_service()
|
||||||
|
.delete(sales_person_id, ())
|
||||||
|
.await?;
|
||||||
|
Ok(Response::builder().status(204).body(Body::empty()).unwrap())
|
||||||
|
})
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue