449 lines
17 KiB
Rust
449 lines
17 KiB
Rust
use std::{collections::HashMap, sync::Arc};
|
|
|
|
use axum::{extract::{Path, State}, Json};
|
|
use reqwest::{StatusCode};
|
|
use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set}, ColumnTrait, EntityTrait, JoinType, QueryFilter, QuerySelect, RelationTrait, TryIntoModel};
|
|
use serde::{Deserialize, Serialize};
|
|
use utoipa::IntoParams;
|
|
|
|
use crate::{entities::{book, book_instance, prelude::*}, routes::{auth::Claims, bal}, utils::auth::{user_is_bal_owner, user_is_book_instance_owner, user_is_owner_owner}, AppState};
|
|
|
|
|
|
#[derive(IntoParams)]
|
|
#[into_params(names("id"), parameter_in = Path)]
|
|
#[allow(dead_code)]
|
|
struct BookInstanceByIdParams(u32);
|
|
|
|
#[axum::debug_handler]
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/book_instance/{id}",
|
|
params(BookInstanceByIdParams),
|
|
security(("jwt" = [])),
|
|
responses(
|
|
(status = OK, body = book_instance::Model, description = "Found book instance with corresponding ID in the database"),
|
|
(status = NOT_FOUND, description = "No book instance with this id exists in the database"),
|
|
(status = FORBIDDEN, description = "You don't own the requested book instance"),
|
|
),
|
|
summary = "Get a book instance by its ID",
|
|
description = "Get a book instance from its ID",
|
|
tag = "book-instance-api",
|
|
)]
|
|
pub async fn get_book_instance_by_id(
|
|
State(state): State<Arc<AppState>>,
|
|
claims: Claims,
|
|
Path(id): Path<u32>,
|
|
) -> (StatusCode, Json<Option<book_instance::Model>>) {
|
|
if let Ok(Some(res)) = BookInstance::find_by_id(id).one(state.db_conn.as_ref()).await {
|
|
if !user_is_book_instance_owner(claims.user_id, res.id, state.db_conn.as_ref()).await {
|
|
return (StatusCode::FORBIDDEN, Json(None));
|
|
}
|
|
(StatusCode::OK, Json(Some(res)))
|
|
} else {
|
|
(StatusCode::NOT_FOUND, Json(None))
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
|
|
pub struct BookInstanceCreateParams {
|
|
book_id: u32,
|
|
owner_id: u32,
|
|
bal_id: u32,
|
|
price: f32,
|
|
}
|
|
|
|
#[axum::debug_handler]
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/book_instance",
|
|
request_body = BookInstanceCreateParams,
|
|
security(("jwt" = [])),
|
|
responses(
|
|
(status = CREATED, body = book_instance::Model, description = "Successfully created book instance"),
|
|
(status = FORBIDDEN, description = "You don't own the specified book instance"),
|
|
),
|
|
summary = "Create a new book instance",
|
|
description = "Create a new book instance",
|
|
tag = "book-instance-api",
|
|
)]
|
|
pub async fn create_book_instance(
|
|
State(state): State<Arc<AppState>>,
|
|
claims: Claims,
|
|
Json(instance_payload): Json<BookInstanceCreateParams>,
|
|
) -> (StatusCode, Json<Option<book_instance::Model>>) {
|
|
if !user_is_bal_owner(claims.user_id, instance_payload.bal_id, state.db_conn.as_ref()).await {
|
|
return (StatusCode::FORBIDDEN, Json(None));
|
|
}
|
|
|
|
let book_instance = book_instance::ActiveModel {
|
|
book_id: Set(instance_payload.book_id),
|
|
owner_id: Set(instance_payload.owner_id),
|
|
bal_id: Set(instance_payload.bal_id),
|
|
price: Set(instance_payload.price),
|
|
available: Set(true),
|
|
id: NotSet,
|
|
sold_price: NotSet,
|
|
};
|
|
|
|
let b = book_instance.save(state.db_conn.as_ref()).await;
|
|
match b {
|
|
Err(e) => {
|
|
log::error!(target: "api", "Error while inserting new book instance: {:#?}", e);
|
|
(StatusCode::BAD_REQUEST, Json(None))
|
|
},
|
|
Ok(res) => (StatusCode::CREATED, 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 {
|
|
owner_id: Option<u32>,
|
|
price: Option<f32>,
|
|
}
|
|
|
|
#[axum::debug_handler]
|
|
#[utoipa::path(
|
|
patch,
|
|
path = "/book_instance/{id}",
|
|
params(BookInstanceByIdParams),
|
|
request_body = BookInstanceUpdateParams,
|
|
security(("jwt" = [])),
|
|
responses(
|
|
(status = OK, body = book_instance::Model, description = "Successfully updated book instance"),
|
|
(status = NOT_FOUND, description = "No book instance with specified id was found"),
|
|
(status = FORBIDDEN, description = "You don't own the specified book instance"),
|
|
),
|
|
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>>,
|
|
claims: Claims,
|
|
Path(id): Path<u32>,
|
|
Json(instance_payload): Json<BookInstanceUpdateParams>,
|
|
) -> (StatusCode, Json<Option<book_instance::Model>>) {
|
|
if !user_is_book_instance_owner(claims.user_id, id, state.db_conn.as_ref()).await {
|
|
return (StatusCode::FORBIDDEN, Json(None));
|
|
}
|
|
|
|
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)
|
|
};
|
|
|
|
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: f32,
|
|
}
|
|
|
|
#[axum::debug_handler]
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/book_instance/{id}/sell",
|
|
params(BookInstanceByIdParams),
|
|
request_body = BookInstanceSaleParams,
|
|
security(("jwt" = [])),
|
|
responses(
|
|
(status = OK, body = book_instance::Model, description = "Successfully sold book instance"),
|
|
(status = CONFLICT, description = "The specified book instance is not available"),
|
|
(status = NOT_FOUND, description = "No book instance with specified id was found"),
|
|
(status = FORBIDDEN, description = "You don't own the specified book instance"),
|
|
),
|
|
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>>,
|
|
claims: Claims,
|
|
Path(id): Path<u32>,
|
|
Json(instance_payload): Json<BookInstanceSaleParams>,
|
|
) -> (StatusCode, Json<Option<book_instance::Model>>) {
|
|
if !user_is_book_instance_owner(claims.user_id, id, state.db_conn.as_ref()).await {
|
|
return (StatusCode::FORBIDDEN, Json(None));
|
|
}
|
|
|
|
if let Ok(Some(book_instance)) = BookInstance::find_by_id(id).one(state.db_conn.as_ref()).await {
|
|
if !book_instance.available {
|
|
return (StatusCode::CONFLICT, Json(None));
|
|
}
|
|
let mut book_instance: book_instance::ActiveModel = book_instance.into();
|
|
book_instance.sold_price = Set(Some(instance_payload.price));
|
|
book_instance.available = Set(false);
|
|
|
|
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/sell/bulk",
|
|
request_body = HashMap<u32, f32>,
|
|
security(("jwt" = [])),
|
|
responses(
|
|
(status = OK, description = "Successfully sold book instances"),
|
|
(status = CONFLICT, description = "One of the specified book instances is not available"),
|
|
(status = NOT_FOUND, description = "One of the specified book instances was not found"),
|
|
(status = FORBIDDEN, description = "You don't own one of the specified book instance"),
|
|
),
|
|
summary = "Sell book instances in bulk",
|
|
description = "Sell book instances in bulk. Payload: {book_instance_id: sold_price, ...}",
|
|
tag = "book-instance-api",
|
|
)]
|
|
pub async fn bulk_sell_book_instance(
|
|
State(state): State<Arc<AppState>>,
|
|
claims: Claims,
|
|
Json(payload): Json<HashMap<u32, f32>>,
|
|
) -> StatusCode {
|
|
// Here, we trust the client inputs valid book instances most of the time, so we already
|
|
// allocate vector memory the size of the input map
|
|
let mut book_instance_list: Vec<(book_instance::Model, f32)> = Vec::with_capacity(payload.len());
|
|
|
|
for instance in payload {
|
|
if let Ok(Some(book_instance)) = BookInstance::find_by_id(instance.0).one(state.db_conn.as_ref()).await {
|
|
if !user_is_bal_owner(claims.user_id, book_instance.bal_id, state.db_conn.as_ref()).await {
|
|
return StatusCode::FORBIDDEN
|
|
} else if !book_instance.available {
|
|
return StatusCode::CONFLICT
|
|
} else {
|
|
book_instance_list.push((book_instance, instance.1));
|
|
}
|
|
} else {
|
|
return StatusCode::NOT_FOUND
|
|
}
|
|
}
|
|
|
|
for instance in book_instance_list {
|
|
let mut book_instance: book_instance::ActiveModel = instance.0.into();
|
|
book_instance.sold_price = Set(Some(instance.1));
|
|
book_instance.available = Set(false);
|
|
|
|
let _ = book_instance.update(state.db_conn.as_ref()).await.unwrap();
|
|
}
|
|
StatusCode::OK
|
|
}
|
|
|
|
#[axum::debug_handler]
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/book_instance/bulk",
|
|
request_body = Vec<BookInstanceCreateParams>,
|
|
security(("jwt" = [])),
|
|
responses(
|
|
(status = CREATED, description = "Successfully created book instances"),
|
|
(status = FORBIDDEN, description = "You don't own at least one specified bal of the book instances sent"),
|
|
),
|
|
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>>,
|
|
claims: Claims,
|
|
Json(instance_payload): Json<Vec<BookInstanceCreateParams>>,
|
|
) -> StatusCode {
|
|
for i in &instance_payload {
|
|
if !user_is_bal_owner(claims.user_id, i.bal_id, state.db_conn.as_ref()).await {
|
|
return StatusCode::FORBIDDEN;
|
|
}
|
|
}
|
|
|
|
let instances = instance_payload
|
|
.into_iter()
|
|
.map(|p| {
|
|
book_instance::ActiveModel {
|
|
book_id: Set(p.book_id),
|
|
owner_id: Set(p.owner_id),
|
|
bal_id: Set(p.bal_id),
|
|
price: Set(p.price),
|
|
available: Set(true),
|
|
id: NotSet,
|
|
sold_price: NotSet
|
|
}
|
|
});
|
|
|
|
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::CREATED
|
|
}
|
|
}
|
|
|
|
#[derive(IntoParams)]
|
|
#[into_params(names("bal_id", "owner_id"), parameter_in = Path)]
|
|
#[allow(dead_code)]
|
|
struct BalOwnerByIdParams(u32, u32);
|
|
|
|
#[axum::debug_handler]
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/bal/{bal_id}/owner/{owner_id}/book_instances",
|
|
params(BalOwnerByIdParams),
|
|
security(("jwt" = [])),
|
|
responses(
|
|
(status = OK, body = Vec<book_instance::Model>, description = "Found book instances in the database"),
|
|
(status = FORBIDDEN, description = "You do not own the specified owner"),
|
|
),
|
|
summary = "Get books instances from an owner in a bal",
|
|
description = "Lists all book instances an owner has in a bal. WARNING: If the bal or owner don't exist, the endpoint will return 200/OK with an empty list",
|
|
tag = "book-instance-api",
|
|
)]
|
|
pub async fn get_bal_owner_book_instances(
|
|
State(state): State<Arc<AppState>>,
|
|
claims: Claims,
|
|
Path((bal_id, owner_id)): Path<(u32, u32)>,
|
|
) -> (StatusCode, Json<Vec<book_instance::Model>>) {
|
|
if !user_is_owner_owner(claims.user_id, owner_id, state.db_conn.as_ref()).await {
|
|
return (StatusCode::FORBIDDEN, Json(vec![]));
|
|
}// If a user owns an owner, it will own the bal the owner has books in,
|
|
// so checking for bal ownership is unnecessary
|
|
if let Ok(res) = BookInstance::find()
|
|
.filter(book_instance::Column::BalId.eq(bal_id))
|
|
.filter(book_instance::Column::OwnerId.eq(owner_id))
|
|
.all(state.db_conn.as_ref()).await
|
|
{
|
|
(StatusCode::OK, Json(res))
|
|
} else {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(vec![]))
|
|
}
|
|
}
|
|
|
|
#[derive(IntoParams)]
|
|
#[into_params(names("bal_id", "ean"), parameter_in = Path)]
|
|
#[allow(dead_code)]
|
|
struct BalBookByEanParams(u32, String);
|
|
|
|
#[axum::debug_handler]
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/bal/{bal_id}/ean/{ean}/book_instances",
|
|
params(BalBookByEanParams),
|
|
security(("jwt" = [])),
|
|
responses(
|
|
(status = OK, body = Vec<book_instance::Model>, description = "Found book instances in the database"),
|
|
(status = FORBIDDEN, description = "You do not own the specified bal"),
|
|
),
|
|
summary = "Get books instances with the specified ean in a bal",
|
|
description = "Lists all book instances with the specified ean in a bal",
|
|
tag = "book-instance-api",
|
|
)]
|
|
pub async fn get_bal_book_instances_by_ean(
|
|
State(state): State<Arc<AppState>>,
|
|
claims: Claims,
|
|
Path((bal_id, ean)): Path<(u32, String)>,
|
|
) -> (StatusCode, Json<Vec<book_instance::Model>>) {
|
|
if !user_is_bal_owner(claims.user_id, bal_id, state.db_conn.as_ref()).await {
|
|
return (StatusCode::FORBIDDEN, Json(vec![]));
|
|
}
|
|
if let Ok(res) = BookInstance::find()
|
|
.filter(book_instance::Column::BalId.eq(bal_id))
|
|
.join(JoinType::InnerJoin, book_instance::Relation::Book.def())
|
|
.filter(book::Column::Ean.eq(ean))
|
|
.all(state.db_conn.as_ref()).await
|
|
{
|
|
(StatusCode::OK, Json(res))
|
|
} else {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(vec![]))
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
|
|
pub struct BookInstanceSearchParams {
|
|
title: Option<String>,
|
|
author: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize, utoipa::ToSchema)]
|
|
pub struct BookInstanceWithBook {
|
|
book_instance: book_instance::Model,
|
|
book: book::Model
|
|
}
|
|
|
|
#[axum::debug_handler]
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/bal/{id}/search",
|
|
params(bal::BalByIdParams),
|
|
request_body = BookInstanceSearchParams,
|
|
security(("jwt" = [])),
|
|
responses(
|
|
(status = OK, body = Vec<BookInstanceWithBook>, description = "Found book instances in the database"),
|
|
(status = FORBIDDEN, description = "You do not own the specified bal"),
|
|
),
|
|
summary = "Search a BAL for books instances",
|
|
description = "Lists all book instances that match the requested parameters in a bal",
|
|
tag = "book-instance-api",
|
|
)]
|
|
pub async fn search_bal_book_instances(
|
|
State(state): State<Arc<AppState>>,
|
|
claims: Claims,
|
|
Path(bal_id): Path<u32>,
|
|
Json(payload): Json<BookInstanceSearchParams>,
|
|
) -> (StatusCode, Json<Option<Vec<BookInstanceWithBook>>>) {
|
|
if !user_is_bal_owner(claims.user_id, bal_id, state.db_conn.as_ref()).await {
|
|
return (StatusCode::FORBIDDEN, Json(None));
|
|
}
|
|
|
|
let mut search_query = BookInstance::find()
|
|
.filter(book_instance::Column::BalId.eq(bal_id))
|
|
.filter(book_instance::Column::Available.eq(true))
|
|
.join(JoinType::InnerJoin, book_instance::Relation::Book.def());
|
|
if let Some(title) = payload.title {
|
|
search_query = search_query.filter(book::Column::Title.like(format!("%{}%", title)))
|
|
}
|
|
if let Some(author) = payload.author {
|
|
search_query = search_query.filter(book::Column::Author.like(format!("%{}%", author)))
|
|
}
|
|
|
|
if let Ok(res) = search_query.all(state.db_conn.as_ref()).await
|
|
{
|
|
let mut book_id_map = HashMap::new();
|
|
for instance in &res {
|
|
if book_id_map.get(&instance.book_id).is_none() {
|
|
book_id_map.insert(instance.book_id, Book::find_by_id(instance.book_id).one(state.db_conn.as_ref()).await.unwrap().unwrap());
|
|
}
|
|
}
|
|
return (StatusCode::OK, Json(Some(res.iter().map(|i| BookInstanceWithBook {
|
|
book: book_id_map.get(&i.book_id).unwrap().clone(),
|
|
book_instance: i.clone()
|
|
}).collect())))
|
|
} else {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(None))
|
|
}
|
|
}
|