Compare commits

...

6 commits

Author SHA1 Message Date
Ninjdai
d8c29e1ec8 feat: Book instance bulk creation endpoint 2025-08-01 14:46:04 +02:00
Ninjdai
62a1d9e2a7 feat: Book instance PATCH route 2025-08-01 14:28:41 +02:00
Ninjdai
1c4abab453 feat: Owner PATCH route 2025-08-01 14:00:03 +02:00
Ninjdai
8a7cfac885 feat: outbound websocket messages logging 2025-08-01 13:03:28 +02:00
Ninjdai
00b38c4989 feat: list owners endpoint (/owners) 2025-08-01 13:01:28 +02:00
Ninjdai
53c95f90fd refactor: remove unused imports 2025-08-01 11:25:27 +02:00
5 changed files with 234 additions and 5 deletions

View file

@ -69,12 +69,21 @@ async fn main() {
});
let (router, mut api) = OpenApiRouter::new()
// Book API
.routes(routes!(routes::book::get_book_by_ean))
.routes(routes!(routes::book::get_book_by_id))
// Book Instance API
.routes(routes!(routes::book_instance::get_book_instance_by_id))
.routes(routes!(routes::book_instance::create_book_instance))
.routes(routes!(routes::book_instance::update_book_instance))
.routes(routes!(routes::book_instance::sell_book_instance))
.routes(routes!(routes::book_instance::bulk_create_book_instance))
// Owner API
.routes(routes!(routes::owner::get_owner_by_id))
.routes(routes!(routes::owner::create_owner))
.routes(routes!(routes::owner::update_owner))
.routes(routes!(routes::owner::get_owners))
// Misc
.routes(routes!(routes::websocket::ws_handler))
.route("/", get(index))
.with_state(shared_state)

View file

@ -17,7 +17,7 @@ struct BookInstanceByIdParams(u32);
#[axum::debug_handler]
#[utoipa::path(
get,
path = "/book_instance/id/{id}",
path = "/book_instance/{id}",
params(BookInstanceByIdParams),
responses(
(status = OK, body = book_instance::Model, description = "Found book instance with corresponding ID in the database"),
@ -79,3 +79,140 @@ pub async fn create_book_instance(
Ok(res) => (StatusCode::OK, Json(Some(res.try_into_model().expect("All fields should be set once the book instance is saved"))))
}
}
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
pub struct BookInstanceUpdateParams {
status: Option<book_instance::BookStatus>,
owner_id: Option<u32>,
price: Option<i32>,
}
#[axum::debug_handler]
#[utoipa::path(
patch,
path = "/book_instance/{id}",
params(BookInstanceByIdParams),
request_body = BookInstanceUpdateParams,
responses(
(status = OK, body = book_instance::Model, description = "Successfully updated book instance"),
(status = NOT_FOUND, description = "No book instance with specified id was found"),
),
summary = "Update a book instance",
description = "Update a book instance",
tag = "book-instance-api",
)]
pub async fn update_book_instance(
State(state): State<Arc<AppState>>,
Path(id): Path<u32>,
Json(instance_payload): Json<BookInstanceUpdateParams>,
) -> (StatusCode, Json<Option<book_instance::Model>>) {
if let Ok(Some(book_instance)) = BookInstance::find_by_id(id).one(state.db_conn.as_ref()).await {
let mut book_instance: book_instance::ActiveModel = book_instance.into();
book_instance.price = match instance_payload.price {
None => book_instance.price,
Some(v) => Set(v)
};
book_instance.owner_id = match instance_payload.owner_id {
None => book_instance.owner_id,
Some(v) => Set(v)
};
book_instance.status = match instance_payload.status {
None => book_instance.status,
Some(v) => Set(v)
};
match book_instance.update(state.db_conn.as_ref()).await {
Err(e) => {
log::error!(target: "api", "Error while updating book instance from api: {:#?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(None))
},
Ok(res) => {
let model = res.try_into_model().expect("All fields should be set once the book instance is saved");
(StatusCode::OK, Json(Some(model)))
}
}
} else {
(StatusCode::NOT_FOUND, Json(None))
}
}
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
pub struct BookInstanceSaleParams {
price: i32,
}
#[axum::debug_handler]
#[utoipa::path(
post,
path = "/book_instance/{id}/sell",
params(BookInstanceByIdParams),
request_body = BookInstanceSaleParams,
responses(
(status = OK, body = book_instance::Model, description = "Successfully sold book instance"),
(status = NOT_FOUND, description = "No book instance with specified id was found"),
),
summary = "Sell a book instance",
description = "Sell a book instance",
tag = "book-instance-api",
)]
pub async fn sell_book_instance(
State(state): State<Arc<AppState>>,
Path(id): Path<u32>,
Json(instance_payload): Json<BookInstanceSaleParams>,
) -> (StatusCode, Json<Option<book_instance::Model>>) {
if let Ok(Some(book_instance)) = BookInstance::find_by_id(id).one(state.db_conn.as_ref()).await {
let mut book_instance: book_instance::ActiveModel = book_instance.into();
book_instance.sold_price = Set(Some(instance_payload.price));
book_instance.status = Set(book_instance::BookStatus::Sold);
match book_instance.update(state.db_conn.as_ref()).await {
Err(e) => {
log::error!(target: "api", "Error while selling book instance from api: {:#?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(None))
},
Ok(res) => {
let model = res.try_into_model().expect("All fields should be set once the book instance is saved");
(StatusCode::OK, Json(Some(model)))
}
}
} else {
(StatusCode::NOT_FOUND, Json(None))
}
}
#[axum::debug_handler]
#[utoipa::path(
post,
path = "/book_instance/bulk",
request_body = Vec<BookInstanceCreateParams>,
responses(
(status = OK, description = "Successfully created book instances"),
),
summary = "Create new book instances in bulk",
description = "Create new book instances in bulk",
tag = "book-instance-api",
)]
pub async fn bulk_create_book_instance(
State(state): State<Arc<AppState>>,
Json(instance_payload): Json<Vec<BookInstanceCreateParams>>,
) -> StatusCode {
let instances = instance_payload
.into_iter()
.map(|p| book_instance::ActiveModel {
book_id: Set(p.book_id),
owner_id: Set(p.owner_id),
price: Set(p.price),
status: Set(book_instance::BookStatus::Available),
..Default::default()
});
match BookInstance::insert_many(instances).exec(state.db_conn.as_ref()).await {
Err(e) => {
log::error!(target: "api", "Error while bulk inserting new book instances: {:#?}", e);
StatusCode::INTERNAL_SERVER_ERROR
},
Ok(_) => StatusCode::OK
}
}

View file

@ -2,7 +2,7 @@ use std::sync::Arc;
use axum::{extract::{Path, State}, Json};
use reqwest::{StatusCode};
use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set}, EntityTrait, TryIntoModel};
use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set, Unchanged}, EntityTrait, TryIntoModel};
use serde::{Deserialize, Serialize};
use utoipa::IntoParams;
@ -82,3 +82,86 @@ pub async fn create_owner(
}
}
}
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
pub struct OwnerUpdateParams {
first_name: Option<String>,
last_name: Option<String>,
contact: Option<String>
}
#[axum::debug_handler]
#[utoipa::path(
patch,
path = "/owner/{id}",
params(OwnerByIdParams),
request_body = OwnerUpdateParams,
responses(
(status = OK, body = owner::Model, description = "Successfully updated owner"),
(status = NOT_FOUND, description = "No owner with this id exists in the database")
),
summary = "Update an owner",
description = "Update an owner",
tag = "owner-api",
)]
pub async fn update_owner(
State(state): State<Arc<AppState>>,
Path(id): Path<u32>,
Json(instance_payload): Json<OwnerUpdateParams>,
) -> (StatusCode, Json<Option<owner::Model>>) {
if let Ok(Some(owner)) = Owner::find_by_id(id).one(state.db_conn.as_ref()).await {
let mut owner: owner::ActiveModel = owner.into();
owner.first_name = match instance_payload.first_name {
None => owner.first_name,
Some(v) => Set(v)
};
owner.last_name = match instance_payload.last_name {
None => owner.last_name,
Some(v) => Set(v)
};
owner.contact = match instance_payload.contact {
None => owner.contact,
Some(v) => Set(v)
};
match owner.update(state.db_conn.as_ref()).await {
Err(e) => {
log::error!(target: "api", "Error while updating owner from api: {:#?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(None))
},
Ok(res) => {
let model = res.try_into_model().expect("All fields should be set once the owner is saved");
let _ = state.event_bus.send(Event::WebsocketBroadcast(WebsocketMessage::NewOwner(Arc::new(model.clone()))));
(StatusCode::OK, Json(Some(model)))
}
}
} else {
(StatusCode::NOT_FOUND, Json(None))
}
}
#[axum::debug_handler]
#[utoipa::path(
get,
path = "/owners",
responses(
(status = OK, body = Vec<owner::Model>, description = "List of owners"),
),
summary = "List book owners",
description = "List book owners",
tag = "owner-api",
)]
pub async fn get_owners(
State(state): State<Arc<AppState>>,
) -> (StatusCode, Json<Option<Vec<owner::Model>>>) {
match Owner::find().all(state.db_conn.as_ref()).await {
Err(e) => {
log::error!(target: "api", "Error while getting owner list: {:#?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(None))
}
Ok(owners) => {
(StatusCode::OK, Json(Some(owners)))
}
}
}

View file

@ -5,9 +5,8 @@ use axum::{
ws::{Message, Utf8Bytes, WebSocket, WebSocketUpgrade},
ConnectInfo,
State
}, http, response::IntoResponse
}, response::IntoResponse
};
use reqwest::StatusCode;
use crate::{utils::events, AppState};
@ -73,6 +72,7 @@ async fn handle_socket(mut socket: WebSocket, who: SocketAddr, state: Arc<AppSta
Ok(event) => {
match event {
events::Event::WebsocketBroadcast(message) => {
log::debug!(target: "websocket", "Sent {message:?} to {who}");
let _ = sender.send(Message::Text(Utf8Bytes::from(message.to_json().to_string()))).await;
}
}

View file

@ -9,7 +9,7 @@ pub enum Event {
WebsocketBroadcast(WebsocketMessage)
}
#[derive(Clone)]
#[derive(Clone, Debug)]
pub enum WebsocketMessage {
NewOwner(Arc<owner::Model>)
}