Compare commits
3 commits
c36a38cd7a
...
3320d1400c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3320d1400c | ||
|
|
fc494d2135 | ||
|
|
aff6c429ce |
9 changed files with 175 additions and 14 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -53,6 +53,7 @@ dependencies = [
|
||||||
"openssl",
|
"openssl",
|
||||||
"password-hash",
|
"password-hash",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
|
"quick-xml",
|
||||||
"rand_core 0.9.3",
|
"rand_core 0.9.3",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
|
|
@ -2082,6 +2083,16 @@ dependencies = [
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-xml"
|
||||||
|
version = "0.38.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9845d9dccf565065824e69f9f235fafba1587031eda353c1f1561cd6a6be78f4"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quinn"
|
name = "quinn"
|
||||||
version = "0.11.8"
|
version = "0.11.8"
|
||||||
|
|
|
||||||
|
|
@ -25,4 +25,5 @@ rand_core = { version = "0.9.3", features = ["os_rng"] }
|
||||||
password-hash = { version = "0.5.0", features = ["getrandom"] }
|
password-hash = { version = "0.5.0", features = ["getrandom"] }
|
||||||
jsonwebtoken = "9.3.1"
|
jsonwebtoken = "9.3.1"
|
||||||
openssl = { version = "0.10.73", features = ["vendored"] }
|
openssl = { version = "0.10.73", features = ["vendored"] }
|
||||||
|
quick-xml = { version = "0.38.1", features = ["serialize"] }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,7 @@ async fn run_server(db: Arc<DatabaseConnection>) {
|
||||||
.routes(routes!(routes::book_instance::update_book_instance))
|
.routes(routes!(routes::book_instance::update_book_instance))
|
||||||
.routes(routes!(routes::book_instance::sell_book_instance))
|
.routes(routes!(routes::book_instance::sell_book_instance))
|
||||||
.routes(routes!(routes::book_instance::bulk_create_book_instance))
|
.routes(routes!(routes::book_instance::bulk_create_book_instance))
|
||||||
|
.routes(routes!(routes::book_instance::get_bal_owner_book_instances))
|
||||||
// Owner API
|
// Owner API
|
||||||
.routes(routes!(routes::owner::get_owner_by_id))
|
.routes(routes!(routes::owner::get_owner_by_id))
|
||||||
.routes(routes!(routes::owner::create_owner))
|
.routes(routes!(routes::owner::create_owner))
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ pub struct BalCreateParams {
|
||||||
request_body = BalCreateParams,
|
request_body = BalCreateParams,
|
||||||
security(("jwt" = [])),
|
security(("jwt" = [])),
|
||||||
responses(
|
responses(
|
||||||
(status = OK, body = bal::Model, description = "Successfully created BAL"),
|
(status = CREATED, body = bal::Model, description = "Successfully created BAL"),
|
||||||
),
|
),
|
||||||
summary = "Create a new bal",
|
summary = "Create a new bal",
|
||||||
description = "Create a new bal",
|
description = "Create a new bal",
|
||||||
|
|
@ -80,7 +80,7 @@ pub async fn create_bal(
|
||||||
log::error!(target: "api", "Error while inserting new bal: {:#?}", e);
|
log::error!(target: "api", "Error while inserting new bal: {:#?}", e);
|
||||||
(StatusCode::BAD_REQUEST, Json(None))
|
(StatusCode::BAD_REQUEST, Json(None))
|
||||||
},
|
},
|
||||||
Ok(res) => (StatusCode::OK, Json(Some(res.try_into_model().expect("All fields should be set once the bal is saved"))))
|
Ok(res) => (StatusCode::CREATED, Json(Some(res.try_into_model().expect("All fields should be set once the bal is saved"))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set}, ColumnTrait, EntityT
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::IntoParams;
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
use crate::{entities::{book, prelude::{Book}}, utils::open_library};
|
use crate::{entities::{book, prelude::Book}, utils::{bnf}};
|
||||||
|
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@ pub async fn get_book_by_ean(
|
||||||
if let Ok(Some(res)) = Book::find().filter(book::Column::Ean.eq(&ean)).one(state.db_conn.as_ref()).await {
|
if let Ok(Some(res)) = Book::find().filter(book::Column::Ean.eq(&ean)).one(state.db_conn.as_ref()).await {
|
||||||
(StatusCode::OK, Json(Some(res)))
|
(StatusCode::OK, Json(Some(res)))
|
||||||
} else {
|
} else {
|
||||||
let fetched_book = open_library::fetch_book_by_ean(&state.web_client, &ean).await;
|
let fetched_book = bnf::fetch_book_by_ean(&state.web_client, &ean).await;
|
||||||
if let Some(book) = fetched_book {
|
if let Some(book) = fetched_book {
|
||||||
let res = Book::insert(book.to_active_model()).exec(state.db_conn.as_ref()).await.unwrap();
|
let res = Book::insert(book.to_active_model()).exec(state.db_conn.as_ref()).await.unwrap();
|
||||||
(StatusCode::OK, Json(Some(book::Model {
|
(StatusCode::OK, Json(Some(book::Model {
|
||||||
|
|
@ -101,7 +101,7 @@ pub struct BookCreateParams {
|
||||||
request_body = BookCreateParams,
|
request_body = BookCreateParams,
|
||||||
security(("jwt" = [])),
|
security(("jwt" = [])),
|
||||||
responses(
|
responses(
|
||||||
(status = OK, body = book::Model, description = "Successfully saved book data"),
|
(status = CREATED, body = book::Model, description = "Successfully saved book data"),
|
||||||
(status = CONFLICT, body = book::Model, description = "A book with the same EAN already exists. Replies with the data of the already saved book."),
|
(status = CONFLICT, body = book::Model, description = "A book with the same EAN already exists. Replies with the data of the already saved book."),
|
||||||
),
|
),
|
||||||
summary = "Manually add book data to the database",
|
summary = "Manually add book data to the database",
|
||||||
|
|
@ -129,6 +129,6 @@ pub async fn create_book(
|
||||||
log::error!(target: "api", "Error while inserting new book: {:#?}", e);
|
log::error!(target: "api", "Error while inserting new book: {:#?}", e);
|
||||||
(StatusCode::BAD_REQUEST, Json(None))
|
(StatusCode::BAD_REQUEST, Json(None))
|
||||||
},
|
},
|
||||||
Ok(res) => (StatusCode::OK, Json(Some(res.try_into_model().expect("All fields should be set once the book is saved"))))
|
Ok(res) => (StatusCode::CREATED, Json(Some(res.try_into_model().expect("All fields should be set once the book is saved"))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use axum::{extract::{Path, State}, Json};
|
use axum::{extract::{Path, State}, Json};
|
||||||
use reqwest::{StatusCode};
|
use reqwest::{StatusCode};
|
||||||
use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set}, EntityTrait, TryIntoModel};
|
use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set}, ColumnTrait, EntityTrait, QueryFilter, TryIntoModel};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::IntoParams;
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
use crate::{entities::{book_instance, prelude::*}, routes::auth::Claims, utils::auth::{user_is_bal_owner, user_is_book_instance_owner}, AppState};
|
use crate::{entities::{book_instance, prelude::*}, routes::auth::Claims, utils::auth::{user_is_bal_owner, user_is_book_instance_owner, user_is_owner_owner}, AppState};
|
||||||
|
|
||||||
|
|
||||||
#[derive(IntoParams)]
|
#[derive(IntoParams)]
|
||||||
|
|
@ -59,7 +59,7 @@ pub struct BookInstanceCreateParams {
|
||||||
request_body = BookInstanceCreateParams,
|
request_body = BookInstanceCreateParams,
|
||||||
security(("jwt" = [])),
|
security(("jwt" = [])),
|
||||||
responses(
|
responses(
|
||||||
(status = OK, body = book_instance::Model, description = "Successfully created book instance"),
|
(status = CREATED, body = book_instance::Model, description = "Successfully created book instance"),
|
||||||
(status = FORBIDDEN, description = "You don't own the specified book instance"),
|
(status = FORBIDDEN, description = "You don't own the specified book instance"),
|
||||||
),
|
),
|
||||||
summary = "Create a new book instance",
|
summary = "Create a new book instance",
|
||||||
|
|
@ -91,7 +91,7 @@ pub async fn create_book_instance(
|
||||||
log::error!(target: "api", "Error while inserting new book instance: {:#?}", e);
|
log::error!(target: "api", "Error while inserting new book instance: {:#?}", e);
|
||||||
(StatusCode::BAD_REQUEST, Json(None))
|
(StatusCode::BAD_REQUEST, Json(None))
|
||||||
},
|
},
|
||||||
Ok(res) => (StatusCode::OK, Json(Some(res.try_into_model().expect("All fields should be set once the book instance is saved"))))
|
Ok(res) => (StatusCode::CREATED, Json(Some(res.try_into_model().expect("All fields should be set once the book instance is saved"))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -216,7 +216,7 @@ pub async fn sell_book_instance(
|
||||||
request_body = Vec<BookInstanceCreateParams>,
|
request_body = Vec<BookInstanceCreateParams>,
|
||||||
security(("jwt" = [])),
|
security(("jwt" = [])),
|
||||||
responses(
|
responses(
|
||||||
(status = OK, description = "Successfully created book instances"),
|
(status = CREATED, description = "Successfully created book instances"),
|
||||||
(status = FORBIDDEN, description = "You don't own at least one specified bal of the book instances sent"),
|
(status = FORBIDDEN, description = "You don't own at least one specified bal of the book instances sent"),
|
||||||
),
|
),
|
||||||
summary = "Create new book instances in bulk",
|
summary = "Create new book instances in bulk",
|
||||||
|
|
@ -253,6 +253,45 @@ pub async fn bulk_create_book_instance(
|
||||||
log::error!(target: "api", "Error while bulk inserting new book instances: {:#?}", e);
|
log::error!(target: "api", "Error while bulk inserting new book instances: {:#?}", e);
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
},
|
},
|
||||||
Ok(_) => StatusCode::OK
|
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![]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ pub struct OwnerCreateParams {
|
||||||
request_body = OwnerCreateParams,
|
request_body = OwnerCreateParams,
|
||||||
security(("jwt" = [])),
|
security(("jwt" = [])),
|
||||||
responses(
|
responses(
|
||||||
(status = OK, body = owner::Model, description = "Successfully created owner"),
|
(status = CREATED, body = owner::Model, description = "Successfully created owner"),
|
||||||
),
|
),
|
||||||
summary = "Create a new owner",
|
summary = "Create a new owner",
|
||||||
description = "Create a new owner",
|
description = "Create a new owner",
|
||||||
|
|
@ -88,7 +88,7 @@ pub async fn create_owner(
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
let model = res.try_into_model().expect("All fields should be set once the owner is saved");
|
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()))));
|
let _ = state.event_bus.send(Event::WebsocketBroadcast(WebsocketMessage::NewOwner(Arc::new(model.clone()))));
|
||||||
(StatusCode::OK, Json(Some(model)))
|
(StatusCode::CREATED, Json(Some(model)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
108
src/utils/bnf.rs
Normal file
108
src/utils/bnf.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use sea_orm::ActiveValue::{NotSet, Set};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::entities::book;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Response {
|
||||||
|
#[serde(rename = "numberOfRecords")]
|
||||||
|
records_number: u32,
|
||||||
|
#[serde(rename = "records")]
|
||||||
|
records: Vec<RecordListElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct RecordListElement {
|
||||||
|
#[serde(rename = "record")]
|
||||||
|
record: Record
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Record {
|
||||||
|
#[serde(rename = "recordData")]
|
||||||
|
record_data: RecordData
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct RecordData {
|
||||||
|
#[serde(rename = "record")]
|
||||||
|
record: RecordDataFields
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct RecordDataFields {
|
||||||
|
#[serde(rename = "datafield")]
|
||||||
|
datafields: Vec<DataField>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct DataField {
|
||||||
|
#[serde(rename = "@tag")]
|
||||||
|
tag: String,
|
||||||
|
#[serde(rename = "subfield")]
|
||||||
|
subfields: Vec<SubField>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub struct SubField {
|
||||||
|
#[serde(rename = "@code")]
|
||||||
|
code: String,
|
||||||
|
#[serde(rename = "$text")]
|
||||||
|
value: String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub struct FetchedBook {
|
||||||
|
pub ean: String,
|
||||||
|
pub title: String,
|
||||||
|
pub author: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FetchedBook {
|
||||||
|
pub fn to_active_model(&self) -> book::ActiveModel {
|
||||||
|
book::ActiveModel {
|
||||||
|
id: NotSet,
|
||||||
|
ean: Set(self.ean.clone()),
|
||||||
|
title: Set(self.title.clone()),
|
||||||
|
author: Set(self.author.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_book_by_ean(web_client: &reqwest::Client, ean: &String) -> Option<FetchedBook> {
|
||||||
|
let body = web_client.execute(
|
||||||
|
web_client.get(format!("https://catalogue.bnf.fr/api/SRU?version=1.2&operation=searchRetrieve&query=bib.isbn any \"{ean}\""))
|
||||||
|
.build()
|
||||||
|
.expect("get request creation failed")
|
||||||
|
).await.unwrap();
|
||||||
|
match body.status() {
|
||||||
|
StatusCode::OK => {
|
||||||
|
let res = body.text().await.unwrap().replace("\n", "");
|
||||||
|
log::debug!(target: "api", "BNF book fetch result: {res:#?}");
|
||||||
|
let der: Result<Response, quick_xml::DeError> = quick_xml::de::from_str(&res);
|
||||||
|
match der {
|
||||||
|
Ok(v) => {
|
||||||
|
if v.records_number == 0 {
|
||||||
|
log::debug!(target: "api", "BNF returned 0 records for fetch");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let data_dubfield = v.records.first().unwrap().record_data.record.record.datafields.iter().find(|d| d.tag == "200").unwrap().subfields.clone();
|
||||||
|
Some(FetchedBook {
|
||||||
|
ean: ean.to_string(),
|
||||||
|
title: data_dubfield.iter().find(|p| p.code == "a").unwrap().value.clone(),
|
||||||
|
author: data_dubfield.iter().find(|p| p.code == "f").unwrap().value.clone(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!(target: "api", "Error while deserializing: {e:?}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//https://catalogue.bnf.fr/api/SRU?version=1.2&operation=searchRetrieve&query=bib.isbn%20any%20%{ean}%22
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
pub mod bnf;
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod open_library;
|
pub mod open_library;
|
||||||
|
|
|
||||||
Reference in a new issue