Fixed bincode responses, refactored content_type handling, better error reporting

Fixed issues with Bincode responses not actually being readable, oops. Also fix handling Bincode requests.

Added `TypedCache` for DRYing up GET request content-type handling.

Added `DeserializedBody` for DRYing up POST/PATCH request conent-type handling.

Removed "Unsaved" structs since I could just mutate Posted structs instead.

Added improved error reporting and stopped sending unfiltered interal error data.

Upgraded sqlx to proper 0.4.1 release.
This commit is contained in:
Tyler Hallada 2020-11-14 02:19:33 -05:00
parent 0bc94e4b7d
commit 50184da1f6
16 changed files with 527 additions and 524 deletions

166
Cargo.lock generated
View File

@ -328,22 +328,6 @@ dependencies = [
"proc-macro-hack",
]
[[package]]
name = "core-foundation"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
[[package]]
name = "cpuid-bool"
version = "0.1.2"
@ -473,21 +457,6 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
@ -1082,24 +1051,6 @@ dependencies = [
"twoway",
]
[[package]]
name = "native-tls"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "net2"
version = "0.2.34"
@ -1158,39 +1109,12 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl"
version = "0.10.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4"
dependencies = [
"bitflags",
"cfg-if 0.1.10",
"foreign-types",
"lazy_static",
"libc",
"openssl-sys",
]
[[package]]
name = "openssl-probe"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
[[package]]
name = "openssl-sys"
version = "0.9.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de"
dependencies = [
"autocfg 1.0.0",
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "parking_lot"
version = "0.11.0"
@ -1255,12 +1179,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33"
[[package]]
name = "ppv-lite86"
version = "0.2.8"
@ -1542,16 +1460,6 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
[[package]]
name = "schannel"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
dependencies = [
"lazy_static",
"winapi 0.3.9",
]
[[package]]
name = "scoped-tls"
version = "1.0.0"
@ -1580,29 +1488,6 @@ version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ee459cae272d224928ca09a1df5406da984f263dc544f9f8bde92a8c3dc916"
[[package]]
name = "security-framework"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "serde"
version = "1.0.114"
@ -1737,8 +1622,9 @@ dependencies = [
[[package]]
name = "sqlx"
version = "0.4.0-beta.1"
source = "git+https://github.com/launchbadge/sqlx?branch=master#12b4250454b13fa2699dee9a4c761154ae60ddb6"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1f8eb788e1733bdbf69a8f97087213ebdebd253d4782c686d3cfd586b0a9453"
dependencies = [
"sqlx-core",
"sqlx-macros",
@ -1746,8 +1632,9 @@ dependencies = [
[[package]]
name = "sqlx-core"
version = "0.4.0-beta.1"
source = "git+https://github.com/launchbadge/sqlx?branch=master#12b4250454b13fa2699dee9a4c761154ae60ddb6"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e647268dc1239dd9db2d3103fefd61151971a2214882cff9efea6f60cf50840"
dependencies = [
"ahash 0.5.8",
"atoi",
@ -1777,6 +1664,7 @@ dependencies = [
"parking_lot",
"percent-encoding",
"rand 0.7.3",
"rustls",
"serde",
"serde_json",
"sha-1 0.9.1",
@ -1788,13 +1676,16 @@ dependencies = [
"thiserror",
"url",
"uuid 0.8.1",
"webpki",
"webpki-roots",
"whoami",
]
[[package]]
name = "sqlx-macros"
version = "0.4.0-beta.1"
source = "git+https://github.com/launchbadge/sqlx?branch=master#12b4250454b13fa2699dee9a4c761154ae60ddb6"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7acd32cba35531345f8a94a038874baf00efd0b701c913f5b00d2870b474b64"
dependencies = [
"dotenv",
"either",
@ -1814,13 +1705,13 @@ dependencies = [
[[package]]
name = "sqlx-rt"
version = "0.1.1"
source = "git+https://github.com/launchbadge/sqlx?branch=master#12b4250454b13fa2699dee9a4c761154ae60ddb6"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63fc5454c9dd7aaea3a0eeeb65ca40d06d0d8e7413a8184f7c3a3ffa5056190b"
dependencies = [
"native-tls",
"once_cell",
"tokio",
"tokio-native-tls",
"tokio-rustls",
]
[[package]]
@ -1941,16 +1832,6 @@ dependencies = [
"syn",
]
[[package]]
name = "tokio-native-tls"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd608593a919a8e05a7d1fc6df885e40f6a88d3a70a3a7eff23ff27964eda069"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.14.1"
@ -2207,12 +2088,6 @@ dependencies = [
"serde",
]
[[package]]
name = "vcpkg"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c"
[[package]]
name = "version_check"
version = "0.9.2"
@ -2339,6 +2214,15 @@ dependencies = [
"untrusted",
]
[[package]]
name = "webpki-roots"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f20dea7535251981a9670857150d571846545088359b28e4951d350bdaf179f"
dependencies = [
"webpki",
]
[[package]]
name = "whoami"
version = "0.9.0"

View File

@ -18,7 +18,7 @@ listenfd = "0.3"
mime = "0.3"
openssl-probe = "0.1"
tokio = { version = "0.2", features = ["macros", "rt-threaded", "sync"] }
sqlx = { git = "https://github.com/launchbadge/sqlx", branch = "master", default-features = false, features = [ "runtime-tokio", "macros", "postgres", "chrono", "uuid", "ipnetwork", "json", "migrate", "offline" ] }
sqlx = { version = "0.4.1", default-features = false, features = [ "runtime-tokio-rustls", "macros", "postgres", "chrono", "uuid", "ipnetwork", "json", "migrate", "offline" ] }
warp = { version = "0.2", features = ["compression", "tls"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

View File

@ -1,17 +1,19 @@
use anyhow::Result;
use http::StatusCode;
use hyper::body::Bytes;
use mime::Mime;
use uuid::Uuid;
use warp::reply::{with_header, with_status};
use warp::{Rejection, Reply};
use crate::caches::CACHES;
use crate::models::{InteriorRefList, ListParams, PostedInteriorRefList, UnsavedInteriorRefList};
use crate::caches::{CachedResponse, CACHES};
use crate::models::{InteriorRefList, ListParams, PostedInteriorRefList};
use crate::problem::reject_anyhow;
use crate::Environment;
use super::{
authenticate, check_etag, AcceptHeader, Bincode, ContentType, DataReply, ETagReply, Json,
authenticate, check_etag, AcceptHeader, Bincode, ContentType, DataReply, DeserializedBody,
ETagReply, Json, TypedCache,
};
pub async fn get(
@ -20,12 +22,14 @@ pub async fn get(
accept: Option<AcceptHeader>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept {
Some(accept) if accept.accepts_bincode() => {
(ContentType::Bincode, &CACHES.interior_ref_list_bin)
}
_ => (ContentType::Json, &CACHES.interior_ref_list),
};
let TypedCache {
content_type,
cache,
} = TypedCache::<i32, CachedResponse>::pick_cache(
accept,
&CACHES.interior_ref_list_bin,
&CACHES.interior_ref_list,
);
let response = cache
.get_response(id, || async {
let interior_ref_list = InteriorRefList::get(&env.db, id).await?;
@ -50,13 +54,14 @@ pub async fn get_by_shop_id(
accept: Option<AcceptHeader>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept {
Some(accept) if accept.accepts_bincode() => (
ContentType::Bincode,
&CACHES.interior_ref_list_by_shop_id_bin,
),
_ => (ContentType::Json, &CACHES.interior_ref_list_by_shop_id),
};
let TypedCache {
content_type,
cache,
} = TypedCache::<i32, CachedResponse>::pick_cache(
accept,
&CACHES.interior_ref_list_by_shop_id_bin,
&CACHES.interior_ref_list_by_shop_id,
);
let response = cache
.get_response(shop_id, || async {
let interior_ref_list = InteriorRefList::get_by_shop_id(&env.db, shop_id).await?;
@ -81,12 +86,14 @@ pub async fn list(
accept: Option<AcceptHeader>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept {
Some(accept) if accept.accepts_bincode() => {
(ContentType::Bincode, &CACHES.list_interior_ref_lists_bin)
}
_ => (ContentType::Json, &CACHES.list_interior_ref_lists),
};
let TypedCache {
content_type,
cache,
} = TypedCache::<ListParams, CachedResponse>::pick_cache(
accept,
&CACHES.list_interior_ref_lists_bin,
&CACHES.list_interior_ref_lists,
);
let response = cache
.get_response(list_params.clone(), || async {
let interior_ref_lists = InteriorRefList::list(&env.db, &list_params).await?;
@ -107,24 +114,19 @@ pub async fn list(
}
pub async fn create(
interior_ref_list: PostedInteriorRefList,
bytes: Bytes,
api_key: Option<Uuid>,
content_type: Option<Mime>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let content_type = match content_type {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => {
ContentType::Bincode
}
_ => ContentType::Json,
};
let DeserializedBody {
body: mut interior_ref_list,
content_type,
} = DeserializedBody::<PostedInteriorRefList>::from_bytes(bytes, content_type)
.map_err(reject_anyhow)?;
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let unsaved_interior_ref_list = UnsavedInteriorRefList {
owner_id,
shop_id: interior_ref_list.shop_id,
ref_list: interior_ref_list.ref_list,
};
let saved_interior_ref_list = InteriorRefList::create(unsaved_interior_ref_list, &env.db)
interior_ref_list.owner_id = Some(owner_id);
let saved_interior_ref_list = InteriorRefList::create(interior_ref_list, &env.db)
.await
.map_err(reject_anyhow)?;
let url = saved_interior_ref_list
@ -159,17 +161,16 @@ pub async fn create(
pub async fn update(
id: i32,
interior_ref_list: PostedInteriorRefList,
bytes: Bytes,
api_key: Option<Uuid>,
content_type: Option<Mime>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let content_type = match content_type {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => {
ContentType::Bincode
}
_ => ContentType::Json,
};
let DeserializedBody {
body: interior_ref_list,
content_type,
} = DeserializedBody::<PostedInteriorRefList>::from_bytes(bytes, content_type)
.map_err(reject_anyhow)?;
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let updated_interior_ref_list =
InteriorRefList::update(interior_ref_list, &env.db, owner_id, id)
@ -209,17 +210,16 @@ pub async fn update(
pub async fn update_by_shop_id(
shop_id: i32,
interior_ref_list: PostedInteriorRefList,
bytes: Bytes,
api_key: Option<Uuid>,
content_type: Option<Mime>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let content_type = match content_type {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => {
ContentType::Bincode
}
_ => ContentType::Json,
};
let DeserializedBody {
body: interior_ref_list,
content_type,
} = DeserializedBody::<PostedInteriorRefList>::from_bytes(bytes, content_type)
.map_err(reject_anyhow)?;
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let updated_interior_ref_list =
InteriorRefList::update_by_shop_id(interior_ref_list, &env.db, owner_id, shop_id)

View File

@ -1,17 +1,19 @@
use anyhow::Result;
use http::StatusCode;
use hyper::body::Bytes;
use mime::Mime;
use uuid::Uuid;
use warp::reply::{with_header, with_status};
use warp::{Rejection, Reply};
use crate::caches::CACHES;
use crate::models::{ListParams, MerchandiseList, PostedMerchandiseList, UnsavedMerchandiseList};
use crate::caches::{CachedResponse, CACHES};
use crate::models::{ListParams, MerchandiseList, PostedMerchandiseList};
use crate::problem::reject_anyhow;
use crate::Environment;
use super::{
authenticate, check_etag, AcceptHeader, Bincode, ContentType, DataReply, ETagReply, Json,
authenticate, check_etag, AcceptHeader, Bincode, ContentType, DataReply, DeserializedBody,
ETagReply, Json, TypedCache,
};
pub async fn get(
@ -20,12 +22,14 @@ pub async fn get(
accept: Option<AcceptHeader>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept {
Some(accept) if accept.accepts_bincode() => {
(ContentType::Bincode, &CACHES.merchandise_list_bin)
}
_ => (ContentType::Json, &CACHES.merchandise_list),
};
let TypedCache {
content_type,
cache,
} = TypedCache::<i32, CachedResponse>::pick_cache(
accept,
&CACHES.merchandise_list_bin,
&CACHES.merchandise_list,
);
let response = cache
.get_response(id, || async {
let merchandise_list = MerchandiseList::get(&env.db, id).await?;
@ -50,12 +54,14 @@ pub async fn get_by_shop_id(
accept: Option<AcceptHeader>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept {
Some(accept) if accept.accepts_bincode() => {
(ContentType::Bincode, &CACHES.merchandise_list_bin)
}
_ => (ContentType::Json, &CACHES.merchandise_list),
};
let TypedCache {
content_type,
cache,
} = TypedCache::<i32, CachedResponse>::pick_cache(
accept,
&CACHES.merchandise_list_by_shop_id_bin,
&CACHES.merchandise_list_by_shop_id,
);
let response = cache
.get_response(shop_id, || async {
let merchandise_list = MerchandiseList::get_by_shop_id(&env.db, shop_id).await?;
@ -80,12 +86,14 @@ pub async fn list(
accept: Option<AcceptHeader>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept {
Some(accept) if accept.accepts_bincode() => {
(ContentType::Bincode, &CACHES.list_merchandise_lists_bin)
}
_ => (ContentType::Json, &CACHES.list_merchandise_lists),
};
let TypedCache {
content_type,
cache,
} = TypedCache::<ListParams, CachedResponse>::pick_cache(
accept,
&CACHES.list_merchandise_lists_bin,
&CACHES.list_merchandise_lists,
);
let response = cache
.get_response(list_params.clone(), || async {
let merchandise_lists = MerchandiseList::list(&env.db, &list_params).await?;
@ -105,24 +113,19 @@ pub async fn list(
}
pub async fn create(
merchandise_list: PostedMerchandiseList,
bytes: Bytes,
api_key: Option<Uuid>,
content_type: Option<Mime>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let content_type = match content_type {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => {
ContentType::Bincode
}
_ => ContentType::Json,
};
let DeserializedBody {
body: mut merchandise_list,
content_type,
} = DeserializedBody::<PostedMerchandiseList>::from_bytes(bytes, content_type)
.map_err(reject_anyhow)?;
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let unsaved_merchandise_list = UnsavedMerchandiseList {
owner_id,
shop_id: merchandise_list.shop_id,
form_list: merchandise_list.form_list,
};
let saved_merchandise_list = MerchandiseList::create(unsaved_merchandise_list, &env.db)
merchandise_list.owner_id = Some(owner_id);
let saved_merchandise_list = MerchandiseList::create(merchandise_list, &env.db)
.await
.map_err(reject_anyhow)?;
let url = saved_merchandise_list
@ -156,17 +159,16 @@ pub async fn create(
pub async fn update(
id: i32,
merchandise_list: PostedMerchandiseList,
bytes: Bytes,
api_key: Option<Uuid>,
content_type: Option<Mime>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let content_type = match content_type {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => {
ContentType::Bincode
}
_ => ContentType::Json,
};
let DeserializedBody {
body: merchandise_list,
content_type,
} = DeserializedBody::<PostedMerchandiseList>::from_bytes(bytes, content_type)
.map_err(reject_anyhow)?;
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let updated_merchandise_list = MerchandiseList::update(merchandise_list, &env.db, owner_id, id)
.await
@ -205,17 +207,16 @@ pub async fn update(
pub async fn update_by_shop_id(
shop_id: i32,
merchandise_list: PostedMerchandiseList,
bytes: Bytes,
api_key: Option<Uuid>,
content_type: Option<Mime>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let content_type = match content_type {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => {
ContentType::Bincode
}
_ => ContentType::Json,
};
let DeserializedBody {
body: merchandise_list,
content_type,
} = DeserializedBody::<PostedMerchandiseList>::from_bytes(bytes, content_type)
.map_err(reject_anyhow)?;
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let updated_merchandise_list =
MerchandiseList::update_by_shop_id(merchandise_list, &env.db, owner_id, shop_id)

View File

@ -1,3 +1,5 @@
use std::fmt::Debug;
use std::hash::Hash;
use std::marker::PhantomData;
use std::str::FromStr;
@ -5,10 +7,11 @@ use anyhow::{anyhow, Error, Result};
use http::header::{HeaderValue, CONTENT_TYPE, ETAG, SERVER};
use http::StatusCode;
use http_api_problem::HttpApiProblem;
use hyper::body::Bytes;
use mime::{FromStrError, Mime};
use seahash::hash;
use serde::Serialize;
use tracing::{error, instrument, warn};
use serde::{de::DeserializeOwned, Serialize};
use tracing::{debug, error, instrument, warn};
use uuid::Uuid;
use warp::reply::Response;
use warp::Reply;
@ -19,7 +22,7 @@ pub mod owner;
pub mod shop;
pub mod transaction;
use super::caches::{CachedResponse, CACHES};
use super::caches::{Cache, CachedResponse, CACHES};
use super::problem::{unauthorized_no_api_key, unauthorized_no_owner};
use super::Environment;
@ -184,3 +187,76 @@ impl AcceptHeader {
self.mimes.contains(&mime::APPLICATION_OCTET_STREAM)
}
}
pub struct DeserializedBody<T> {
body: T,
content_type: ContentType,
}
impl<T: DeserializeOwned> DeserializedBody<T> {
pub fn from_bytes(bytes: Bytes, content_type: Option<Mime>) -> Result<Self> {
match content_type {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => {
debug!(
content_type = ?ContentType::Bincode,
"deserializing body as bincode"
);
Ok(Self {
content_type: ContentType::Bincode,
body: bincode::deserialize(&bytes)?,
})
}
_ => {
debug!(
content_type = ?ContentType::Json,
"deserializing body as json"
);
Ok(Self {
content_type: ContentType::Json,
body: serde_json::from_slice(&bytes)?,
})
}
}
}
}
pub struct TypedCache<'a, K, V>
where
K: Eq + Hash + Debug,
V: Clone,
{
cache: &'a Cache<K, V>,
content_type: ContentType,
}
impl<'a, K, V> TypedCache<'a, K, V>
where
K: Eq + Hash + Debug,
V: Clone,
{
pub fn pick_cache(
accept: Option<AcceptHeader>,
bincode_cache: &'a Cache<K, V>,
json_cache: &'a Cache<K, V>,
) -> Self {
match accept {
Some(accept) if accept.accepts_bincode() => {
debug!(
content_type = ?ContentType::Bincode,
"serializing body as bincode"
);
Self {
content_type: ContentType::Bincode,
cache: bincode_cache,
}
}
_ => {
debug!(content_type = ?ContentType::Json, "serializing body as json");
Self {
content_type: ContentType::Json,
cache: json_cache,
}
}
}
}
}

View File

@ -1,5 +1,6 @@
use anyhow::Result;
use http::StatusCode;
use hyper::body::Bytes;
use ipnetwork::IpNetwork;
use mime::Mime;
use std::net::SocketAddr;
@ -7,13 +8,14 @@ use uuid::Uuid;
use warp::reply::{with_header, with_status};
use warp::{Rejection, Reply};
use crate::caches::CACHES;
use crate::models::{ListParams, Owner, PostedOwner, UnsavedOwner};
use crate::caches::{CachedResponse, CACHES};
use crate::models::{FullPostedOwner, ListParams, Owner, PostedOwner};
use crate::problem::{reject_anyhow, unauthorized_no_api_key};
use crate::Environment;
use super::{
authenticate, check_etag, AcceptHeader, Bincode, ContentType, DataReply, ETagReply, Json,
authenticate, check_etag, AcceptHeader, Bincode, ContentType, DataReply, DeserializedBody,
ETagReply, Json, TypedCache,
};
pub async fn get(
@ -22,10 +24,10 @@ pub async fn get(
accept: Option<AcceptHeader>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept {
Some(accept) if accept.accepts_bincode() => (ContentType::Bincode, &CACHES.owner_bin),
_ => (ContentType::Json, &CACHES.owner),
};
let TypedCache {
content_type,
cache,
} = TypedCache::<i32, CachedResponse>::pick_cache(accept, &CACHES.owner_bin, &CACHES.owner);
let response = cache
.get_response(id, || async {
let owner = Owner::get(&env.db, id).await?;
@ -46,10 +48,14 @@ pub async fn list(
accept: Option<AcceptHeader>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept {
Some(accept) if accept.accepts_bincode() => (ContentType::Bincode, &CACHES.list_owners_bin),
_ => (ContentType::Json, &CACHES.list_owners),
};
let TypedCache {
content_type,
cache,
} = TypedCache::<ListParams, CachedResponse>::pick_cache(
accept,
&CACHES.list_owners_bin,
&CACHES.list_owners,
);
let response = cache
.get_response(list_params.clone(), || async {
let owners = Owner::list(&env.db, &list_params).await?;
@ -65,7 +71,7 @@ pub async fn list(
}
pub async fn create(
owner: PostedOwner,
bytes: Bytes,
remote_addr: Option<SocketAddr>,
api_key: Option<Uuid>,
real_ip: Option<IpNetwork>,
@ -73,24 +79,21 @@ pub async fn create(
env: Environment,
) -> Result<impl Reply, Rejection> {
if let Some(api_key) = api_key {
let content_type = match content_type {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => {
ContentType::Bincode
}
_ => ContentType::Json,
};
let unsaved_owner = UnsavedOwner {
let DeserializedBody {
body: owner,
content_type,
} = DeserializedBody::<PostedOwner>::from_bytes(bytes, content_type)
.map_err(reject_anyhow)?;
let owner = FullPostedOwner {
name: owner.name,
mod_version: owner.mod_version,
api_key,
ip_address: match remote_addr {
Some(addr) => Some(IpNetwork::from(addr.ip())),
None => real_ip,
},
name: owner.name,
mod_version: owner.mod_version,
};
let saved_owner = Owner::create(unsaved_owner, &env.db)
.await
.map_err(reject_anyhow)?;
let saved_owner = Owner::create(owner, &env.db).await.map_err(reject_anyhow)?;
let url = saved_owner.url(&env.api_url).map_err(reject_anyhow)?;
let reply: Box<dyn Reply> = match content_type {
ContentType::Bincode => Box::new(
@ -114,17 +117,15 @@ pub async fn create(
pub async fn update(
id: i32,
owner: PostedOwner,
bytes: Bytes,
api_key: Option<Uuid>,
content_type: Option<Mime>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let content_type = match content_type {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => {
ContentType::Bincode
}
_ => ContentType::Json,
};
let DeserializedBody {
body: owner,
content_type,
} = DeserializedBody::<PostedOwner>::from_bytes(bytes, content_type).map_err(reject_anyhow)?;
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let updated_owner = Owner::update(owner, &env.db, owner_id, id)
.await

View File

@ -1,20 +1,22 @@
use anyhow::{anyhow, Result};
use http::StatusCode;
use hyper::body::Bytes;
use mime::Mime;
use uuid::Uuid;
use warp::reply::{with_header, with_status};
use warp::{Rejection, Reply};
use crate::caches::CACHES;
use crate::caches::{CachedResponse, CACHES};
use crate::models::{
InteriorRefList, ListParams, MerchandiseList, PostedShop, Shop, UnsavedInteriorRefList,
UnsavedMerchandiseList, UnsavedShop,
InteriorRefList, ListParams, MerchandiseList, PostedInteriorRefList, PostedMerchandiseList,
PostedShop, Shop,
};
use crate::problem::reject_anyhow;
use crate::Environment;
use super::{
authenticate, check_etag, AcceptHeader, Bincode, ContentType, DataReply, ETagReply, Json,
authenticate, check_etag, AcceptHeader, Bincode, ContentType, DataReply, DeserializedBody,
ETagReply, Json, TypedCache,
};
pub async fn get(
@ -23,10 +25,10 @@ pub async fn get(
accept: Option<AcceptHeader>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept {
Some(accept) if accept.accepts_bincode() => (ContentType::Bincode, &CACHES.shop_bin),
_ => (ContentType::Json, &CACHES.shop),
};
let TypedCache {
content_type,
cache,
} = TypedCache::<i32, CachedResponse>::pick_cache(accept, &CACHES.shop_bin, &CACHES.shop);
let response = cache
.get_response(id, || async {
let shop = Shop::get(&env.db, id).await?;
@ -47,10 +49,14 @@ pub async fn list(
accept: Option<AcceptHeader>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept {
Some(accept) if accept.accepts_bincode() => (ContentType::Bincode, &CACHES.list_shops_bin),
_ => (ContentType::Json, &CACHES.list_shops),
};
let TypedCache {
content_type,
cache,
} = TypedCache::<ListParams, CachedResponse>::pick_cache(
accept,
&CACHES.list_shops_bin,
&CACHES.list_shops,
);
let response = cache
.get_response(list_params.clone(), || async {
let shops = Shop::list(&env.db, &list_params).await?;
@ -66,44 +72,36 @@ pub async fn list(
}
pub async fn create(
shop: PostedShop,
bytes: Bytes,
api_key: Option<Uuid>,
content_type: Option<Mime>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let content_type = match content_type {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => {
ContentType::Bincode
}
_ => ContentType::Json,
};
let DeserializedBody {
body: mut shop,
content_type,
} = DeserializedBody::<PostedShop>::from_bytes(bytes, content_type).map_err(reject_anyhow)?;
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let unsaved_shop = UnsavedShop {
name: shop.name,
description: shop.description,
owner_id,
};
shop.owner_id = Some(owner_id);
let mut tx = env
.db
.begin()
.await
.map_err(|error| reject_anyhow(anyhow!(error)))?;
let saved_shop = Shop::create(unsaved_shop, &mut tx)
.await
.map_err(reject_anyhow)?;
let saved_shop = Shop::create(shop, &mut tx).await.map_err(reject_anyhow)?;
// also save empty interior_ref_list and merchandise_list rows
let interior_ref_list = UnsavedInteriorRefList {
let interior_ref_list = PostedInteriorRefList {
shop_id: saved_shop.id,
owner_id,
owner_id: Some(owner_id),
ref_list: sqlx::types::Json::default(),
};
InteriorRefList::create(interior_ref_list, &mut tx)
.await
.map_err(reject_anyhow)?;
let merchandise_list = UnsavedMerchandiseList {
let merchandise_list = PostedMerchandiseList {
shop_id: saved_shop.id,
owner_id,
owner_id: Some(owner_id),
form_list: sqlx::types::Json::default(),
};
MerchandiseList::create(merchandise_list, &mut tx)
@ -133,27 +131,22 @@ pub async fn create(
pub async fn update(
id: i32,
shop: PostedShop,
bytes: Bytes,
api_key: Option<Uuid>,
content_type: Option<Mime>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let content_type = match content_type {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => {
ContentType::Bincode
}
_ => ContentType::Json,
};
let DeserializedBody {
body: mut shop,
content_type,
} = DeserializedBody::<PostedShop>::from_bytes(bytes, content_type).map_err(reject_anyhow)?;
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let posted_shop = PostedShop {
owner_id: match shop.owner_id {
// allows an owner to transfer ownership of shop to another owner
Some(posted_owner_id) => Some(posted_owner_id),
None => Some(owner_id),
},
..shop
shop.owner_id = match shop.owner_id {
// allows an owner to transfer ownership of shop to another owner
Some(posted_owner_id) => Some(posted_owner_id),
None => Some(owner_id),
};
let updated_shop = Shop::update(posted_shop, &env.db, owner_id, id)
let updated_shop = Shop::update(shop, &env.db, owner_id, id)
.await
.map_err(reject_anyhow)?;
let url = updated_shop.url(&env.api_url).map_err(reject_anyhow)?;

View File

@ -1,19 +1,19 @@
use anyhow::{anyhow, Result};
use http::StatusCode;
use hyper::body::Bytes;
use mime::Mime;
use uuid::Uuid;
use warp::reply::{with_header, with_status};
use warp::{Rejection, Reply};
use crate::caches::CACHES;
use crate::models::{
ListParams, MerchandiseList, PostedTransaction, Transaction, UnsavedTransaction,
};
use crate::caches::{CachedResponse, CACHES};
use crate::models::{ListParams, MerchandiseList, PostedTransaction, Transaction};
use crate::problem::reject_anyhow;
use crate::Environment;
use super::{
authenticate, check_etag, AcceptHeader, Bincode, ContentType, DataReply, ETagReply, Json,
authenticate, check_etag, AcceptHeader, Bincode, ContentType, DataReply, DeserializedBody,
ETagReply, Json, TypedCache,
};
pub async fn get(
@ -22,10 +22,14 @@ pub async fn get(
accept: Option<AcceptHeader>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept {
Some(accept) if accept.accepts_bincode() => (ContentType::Bincode, &CACHES.transaction_bin),
_ => (ContentType::Json, &CACHES.transaction),
};
let TypedCache {
content_type,
cache,
} = TypedCache::<i32, CachedResponse>::pick_cache(
accept,
&CACHES.transaction_bin,
&CACHES.transaction,
);
let response = cache
.get_response(id, || async {
let transaction = Transaction::get(&env.db, id).await?;
@ -48,12 +52,14 @@ pub async fn list(
accept: Option<AcceptHeader>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept {
Some(accept) if accept.accepts_bincode() => {
(ContentType::Bincode, &CACHES.list_transactions_bin)
}
_ => (ContentType::Json, &CACHES.list_transactions),
};
let TypedCache {
content_type,
cache,
} = TypedCache::<ListParams, CachedResponse>::pick_cache(
accept,
&CACHES.list_transactions_bin,
&CACHES.list_transactions,
);
let response = cache
.get_response(list_params.clone(), || async {
let transactions = Transaction::list(&env.db, &list_params).await?;
@ -77,13 +83,14 @@ pub async fn list_by_shop_id(
accept: Option<AcceptHeader>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let (content_type, cache) = match accept {
Some(accept) if accept.accepts_bincode() => (
ContentType::Bincode,
&CACHES.list_transactions_by_shop_id_bin,
),
_ => (ContentType::Json, &CACHES.list_transactions_by_shop_id),
};
let TypedCache {
content_type,
cache,
} = TypedCache::<(i32, ListParams), CachedResponse>::pick_cache(
accept,
&CACHES.list_transactions_by_shop_id_bin,
&CACHES.list_transactions_by_shop_id,
);
let response = cache
.get_response((shop_id, list_params.clone()), || async {
let transactions = Transaction::list_by_shop_id(&env.db, shop_id, &list_params).await?;
@ -101,42 +108,29 @@ pub async fn list_by_shop_id(
}
pub async fn create(
transaction: PostedTransaction,
bytes: Bytes,
api_key: Option<Uuid>,
content_type: Option<Mime>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let content_type = match content_type {
Some(content_type) if content_type == mime::APPLICATION_OCTET_STREAM => {
ContentType::Bincode
}
_ => ContentType::Json,
};
let DeserializedBody {
body: mut transaction,
content_type,
} = DeserializedBody::<PostedTransaction>::from_bytes(bytes, content_type)
.map_err(reject_anyhow)?;
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let unsaved_transaction = UnsavedTransaction {
shop_id: transaction.shop_id,
owner_id,
mod_name: transaction.mod_name,
local_form_id: transaction.local_form_id,
name: transaction.name,
form_type: transaction.form_type,
is_food: transaction.is_food,
price: transaction.price,
is_sell: transaction.is_sell,
quantity: transaction.quantity,
amount: transaction.amount,
};
transaction.owner_id = Some(owner_id);
let mut tx = env
.db
.begin()
.await
.map_err(|error| reject_anyhow(anyhow!(error)))?;
let saved_transaction = Transaction::create(unsaved_transaction, &mut tx)
let saved_transaction = Transaction::create(transaction, &mut tx)
.await
.map_err(reject_anyhow)?;
let quantity_delta = match transaction.is_sell {
true => transaction.quantity,
false => transaction.quantity * -1,
let quantity_delta = match saved_transaction.is_sell {
true => saved_transaction.quantity,
false => saved_transaction.quantity * -1,
};
let updated_merchandise_list = MerchandiseList::update_merchandise_quantity(
&mut tx,

View File

@ -4,9 +4,8 @@ extern crate lazy_static;
use anyhow::Result;
use dotenv::dotenv;
use http::header::SERVER;
use hyper::server::Server;
use hyper::{body::Bytes, server::Server};
use listenfd::ListenFd;
use serde::{de::DeserializeOwned, Serialize};
use sqlx::postgres::PgPoolOptions;
use sqlx::{migrate, Pool, Postgres};
use std::convert::Infallible;
@ -24,10 +23,7 @@ mod models;
mod problem;
use handlers::SERVER_STRING;
use models::{
ListParams, PostedInteriorRefList, PostedMerchandiseList, PostedOwner, PostedShop,
PostedTransaction,
};
use models::ListParams;
#[derive(Debug, Clone)]
pub struct Environment {
@ -47,21 +43,12 @@ impl Environment {
}
}
#[derive(Serialize)]
struct ErrorMessage {
code: u16,
message: String,
}
fn with_env(env: Environment) -> impl Filter<Extract = (Environment,), Error = Infallible> + Clone {
warp::any().map(move || env.clone())
}
fn json_body<T>() -> impl Filter<Extract = (T,), Error = warp::Rejection> + Clone
where
T: Send + DeserializeOwned,
{
warp::body::content_length_limit(1024 * 1024).and(warp::body::json())
fn extract_body_bytes() -> impl Filter<Extract = (Bytes,), Error = warp::Rejection> + Clone {
warp::body::content_length_limit(1024 * 1024).and(warp::body::bytes())
}
#[tokio::main]
@ -100,7 +87,7 @@ async fn main() -> Result<()> {
let create_owner_handler = warp::path("owners").and(
warp::path::end()
.and(warp::post())
.and(json_body::<PostedOwner>())
.and(extract_body_bytes())
.and(warp::addr::remote())
.and(warp::header::optional("api-key"))
.and(warp::header::optional("x-real-ip"))
@ -120,7 +107,7 @@ async fn main() -> Result<()> {
warp::path::param()
.and(warp::path::end())
.and(warp::patch())
.and(json_body::<PostedOwner>())
.and(extract_body_bytes())
.and(warp::header::optional("api-key"))
.and(warp::header::optional("content-type"))
.and(with_env(env.clone()))
@ -147,7 +134,7 @@ async fn main() -> Result<()> {
let create_shop_handler = warp::path("shops").and(
warp::path::end()
.and(warp::post())
.and(json_body::<PostedShop>())
.and(extract_body_bytes())
.and(warp::header::optional("api-key"))
.and(warp::header::optional("content-type"))
.and(with_env(env.clone()))
@ -165,7 +152,7 @@ async fn main() -> Result<()> {
warp::path::param()
.and(warp::path::end())
.and(warp::patch())
.and(json_body::<PostedShop>())
.and(extract_body_bytes())
.and(warp::header::optional("api-key"))
.and(warp::header::optional("content-type"))
.and(with_env(env.clone()))
@ -192,7 +179,7 @@ async fn main() -> Result<()> {
let create_interior_ref_list_handler = warp::path("interior_ref_lists").and(
warp::path::end()
.and(warp::post())
.and(json_body::<PostedInteriorRefList>())
.and(extract_body_bytes())
.and(warp::header::optional("api-key"))
.and(warp::header::optional("content-type"))
.and(with_env(env.clone()))
@ -210,7 +197,7 @@ async fn main() -> Result<()> {
warp::path::param()
.and(warp::path::end())
.and(warp::patch())
.and(json_body::<PostedInteriorRefList>())
.and(extract_body_bytes())
.and(warp::header::optional("api-key"))
.and(warp::header::optional("content-type"))
.and(with_env(env.clone()))
@ -221,7 +208,7 @@ async fn main() -> Result<()> {
.and(warp::path("interior_ref_list"))
.and(warp::path::end())
.and(warp::patch())
.and(json_body::<PostedInteriorRefList>())
.and(extract_body_bytes())
.and(warp::header::optional("api-key"))
.and(warp::header::optional("content-type"))
.and(with_env(env.clone()))
@ -258,7 +245,7 @@ async fn main() -> Result<()> {
let create_merchandise_list_handler = warp::path("merchandise_lists").and(
warp::path::end()
.and(warp::post())
.and(json_body::<PostedMerchandiseList>())
.and(extract_body_bytes())
.and(warp::header::optional("api-key"))
.and(warp::header::optional("content-type"))
.and(with_env(env.clone()))
@ -276,7 +263,7 @@ async fn main() -> Result<()> {
warp::path::param()
.and(warp::path::end())
.and(warp::patch())
.and(json_body::<PostedMerchandiseList>())
.and(extract_body_bytes())
.and(warp::header::optional("api-key"))
.and(warp::header::optional("content-type"))
.and(with_env(env.clone()))
@ -287,7 +274,7 @@ async fn main() -> Result<()> {
.and(warp::path("merchandise_list"))
.and(warp::path::end())
.and(warp::patch())
.and(json_body::<PostedMerchandiseList>())
.and(extract_body_bytes())
.and(warp::header::optional("api-key"))
.and(warp::header::optional("content-type"))
.and(with_env(env.clone()))
@ -324,7 +311,7 @@ async fn main() -> Result<()> {
let create_transaction_handler = warp::path("transactions").and(
warp::path::end()
.and(warp::post())
.and(json_body::<PostedTransaction>())
.and(extract_body_bytes())
.and(warp::header::optional("api-key"))
.and(warp::header::optional("content-type"))
.and(with_env(env.clone()))

View File

@ -9,12 +9,12 @@ use url::Url;
use super::ListParams;
use crate::problem::forbidden_permission;
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(sqlx::FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct InteriorRef {
pub base_mod_name: String,
pub base_local_form_id: i32,
pub base_local_form_id: u32,
pub ref_mod_name: Option<String>,
pub ref_local_form_id: i32,
pub ref_local_form_id: u32,
pub position_x: f32,
pub position_y: f32,
pub position_z: f32,
@ -24,23 +24,16 @@ pub struct InteriorRef {
pub scale: u16,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(sqlx::FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct InteriorRefList {
pub id: i32,
pub shop_id: i32,
pub owner_id: i32,
pub ref_list: serde_json::Value,
pub ref_list: Json<Vec<InteriorRef>>,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UnsavedInteriorRefList {
pub shop_id: i32,
pub owner_id: i32,
pub ref_list: Json<Vec<InteriorRef>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PostedInteriorRefList {
pub shop_id: i32,
@ -64,23 +57,30 @@ impl InteriorRefList {
// TODO: this model will probably never need to be accessed through it's ID, should these methods be removed/unimplemented?
#[instrument(level = "debug", skip(db))]
pub async fn get(db: impl Executor<'_, Database = Postgres>, id: i32) -> Result<Self> {
sqlx::query_as!(Self, "SELECT * FROM interior_ref_lists WHERE id = $1", id)
.fetch_one(db)
.await
.map_err(Error::new)
sqlx::query_as!(
Self,
r#"SELECT id, shop_id, owner_id, created_at, updated_at,
ref_list as "ref_list: Json<Vec<InteriorRef>>"
FROM interior_ref_lists WHERE id = $1"#,
id
)
.fetch_one(db)
.await
.map_err(Error::new)
}
#[instrument(level = "debug", skip(interior_ref_list, db))]
pub async fn create(
interior_ref_list: UnsavedInteriorRefList,
interior_ref_list: PostedInteriorRefList,
db: impl Executor<'_, Database = Postgres>,
) -> Result<Self> {
Ok(sqlx::query_as!(
Self,
"INSERT INTO interior_ref_lists
(shop_id, owner_id, ref_list, created_at, updated_at)
r#"INSERT INTO interior_ref_lists
(shop_id, owner_id, ref_list, created_at, updated_at)
VALUES ($1, $2, $3, now(), now())
RETURNING *",
RETURNING id, shop_id, owner_id, created_at, updated_at,
ref_list as "ref_list: Json<Vec<InteriorRef>>""#,
interior_ref_list.shop_id,
interior_ref_list.owner_id,
serde_json::json!(interior_ref_list.ref_list),
@ -119,10 +119,11 @@ impl InteriorRefList {
let result = if let Some(order_by) = list_params.get_order_by() {
sqlx::query_as!(
Self,
"SELECT * FROM interior_ref_lists
r#"SELECT id, shop_id, owner_id, created_at, updated_at,
ref_list as "ref_list: Json<Vec<InteriorRef>>" FROM interior_ref_lists
ORDER BY $1
LIMIT $2
OFFSET $3",
OFFSET $3"#,
order_by,
list_params.limit.unwrap_or(10),
list_params.offset.unwrap_or(0),
@ -132,9 +133,10 @@ impl InteriorRefList {
} else {
sqlx::query_as!(
Self,
"SELECT * FROM interior_ref_lists
r#"SELECT id, shop_id, owner_id, created_at, updated_at,
ref_list as "ref_list: Json<Vec<InteriorRef>>" FROM interior_ref_lists
LIMIT $1
OFFSET $2",
OFFSET $2"#,
list_params.limit.unwrap_or(10),
list_params.offset.unwrap_or(0),
)
@ -158,11 +160,12 @@ impl InteriorRefList {
if existing_interior_ref_list.owner_id == owner_id {
Ok(sqlx::query_as!(
Self,
"UPDATE interior_ref_lists SET
r#"UPDATE interior_ref_lists SET
ref_list = $2,
updated_at = now()
WHERE id = $1
RETURNING *",
RETURNING id, shop_id, owner_id, created_at, updated_at,
ref_list as "ref_list: Json<Vec<InteriorRef>>""#,
id,
serde_json::json!(interior_ref_list.ref_list),
)
@ -180,8 +183,9 @@ impl InteriorRefList {
) -> Result<Self> {
sqlx::query_as!(
Self,
"SELECT * FROM interior_ref_lists
WHERE shop_id = $1",
r#"SELECT id, shop_id, owner_id, created_at, updated_at,
ref_list as "ref_list: Json<Vec<InteriorRef>>" FROM interior_ref_lists
WHERE shop_id = $1"#,
shop_id,
)
.fetch_one(db)
@ -205,11 +209,12 @@ impl InteriorRefList {
if existing_interior_ref_list.owner_id == owner_id {
Ok(sqlx::query_as!(
Self,
"UPDATE interior_ref_lists SET
r#"UPDATE interior_ref_lists SET
ref_list = $2,
updated_at = now()
WHERE shop_id = $1
RETURNING *",
RETURNING id, shop_id, owner_id, created_at, updated_at,
ref_list as "ref_list: Json<Vec<InteriorRef>>""#,
shop_id,
serde_json::json!(interior_ref_list.ref_list),
)

View File

@ -15,12 +15,12 @@ use crate::problem::forbidden_permission;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Merchandise {
pub mod_name: String,
pub local_form_id: i32,
pub local_form_id: u32,
pub name: String,
pub quantity: i32,
pub form_type: i32,
pub quantity: u32,
pub form_type: u32,
pub is_food: bool,
pub price: i32,
pub price: u32,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
@ -28,18 +28,11 @@ pub struct MerchandiseList {
pub id: i32,
pub shop_id: i32,
pub owner_id: i32,
pub form_list: serde_json::Value,
pub form_list: Json<Vec<Merchandise>>,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UnsavedMerchandiseList {
pub shop_id: i32,
pub owner_id: i32,
pub form_list: Json<Vec<Merchandise>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PostedMerchandiseList {
pub shop_id: i32,
@ -63,23 +56,31 @@ impl MerchandiseList {
// TODO: this model will probably never need to be accessed through it's ID, should these methods be removed/unimplemented?
#[instrument(level = "debug", skip(db))]
pub async fn get(db: impl Executor<'_, Database = Postgres>, id: i32) -> Result<Self> {
sqlx::query_as!(Self, "SELECT * FROM merchandise_lists WHERE id = $1", id)
.fetch_one(db)
.await
.map_err(Error::new)
sqlx::query_as!(
Self,
r#"SELECT id, shop_id, owner_id, created_at, updated_at,
form_list as "form_list: Json<Vec<Merchandise>>"
FROM merchandise_lists
WHERE id = $1"#,
id,
)
.fetch_one(db)
.await
.map_err(Error::new)
}
#[instrument(level = "debug", skip(merchandise_list, db))]
pub async fn create(
merchandise_list: UnsavedMerchandiseList,
merchandise_list: PostedMerchandiseList,
db: impl Executor<'_, Database = Postgres>,
) -> Result<Self> {
Ok(sqlx::query_as!(
Self,
"INSERT INTO merchandise_lists
r#"INSERT INTO merchandise_lists
(shop_id, owner_id, form_list, created_at, updated_at)
VALUES ($1, $2, $3, now(), now())
RETURNING *",
RETURNING id, shop_id, owner_id, created_at, updated_at,
form_list as "form_list: Json<Vec<Merchandise>>""#,
merchandise_list.shop_id,
merchandise_list.owner_id,
serde_json::json!(merchandise_list.form_list),
@ -118,10 +119,12 @@ impl MerchandiseList {
let result = if let Some(order_by) = list_params.get_order_by() {
sqlx::query_as!(
Self,
"SELECT * FROM merchandise_lists
r#"SELECT id, shop_id, owner_id, created_at, updated_at,
form_list as "form_list: Json<Vec<Merchandise>>"
FROM merchandise_lists
ORDER BY $1
LIMIT $2
OFFSET $3",
OFFSET $3"#,
order_by,
list_params.limit.unwrap_or(10),
list_params.offset.unwrap_or(0),
@ -131,9 +134,11 @@ impl MerchandiseList {
} else {
sqlx::query_as!(
Self,
"SELECT * FROM merchandise_lists
r#"SELECT id, shop_id, owner_id, created_at, updated_at,
form_list as "form_list: Json<Vec<Merchandise>>"
FROM merchandise_lists
LIMIT $1
OFFSET $2",
OFFSET $2"#,
list_params.limit.unwrap_or(10),
list_params.offset.unwrap_or(0),
)
@ -157,11 +162,12 @@ impl MerchandiseList {
if existing_merchandise_list.owner_id == owner_id {
Ok(sqlx::query_as!(
Self,
"UPDATE merchandise_lists SET
r#"UPDATE merchandise_lists SET
form_list = $2,
updated_at = now()
WHERE id = $1
RETURNING *",
RETURNING id, shop_id, owner_id, created_at, updated_at,
form_list as "form_list: Json<Vec<Merchandise>>""#,
id,
serde_json::json!(merchandise_list.form_list),
)
@ -179,8 +185,10 @@ impl MerchandiseList {
) -> Result<Self> {
sqlx::query_as!(
Self,
"SELECT * FROM merchandise_lists
WHERE shop_id = $1",
r#"SELECT id, shop_id, owner_id, created_at, updated_at,
form_list as "form_list: Json<Vec<Merchandise>>"
FROM merchandise_lists
WHERE shop_id = $1"#,
shop_id,
)
.fetch_one(db)
@ -204,11 +212,12 @@ impl MerchandiseList {
if existing_merchandise_list.owner_id == owner_id {
Ok(sqlx::query_as!(
Self,
"UPDATE merchandise_lists SET
r#"UPDATE merchandise_lists SET
form_list = $2,
updated_at = now()
WHERE shop_id = $1
RETURNING *",
RETURNING id, shop_id, owner_id, created_at, updated_at,
form_list as "form_list: Json<Vec<Merchandise>>""#,
shop_id,
serde_json::json!(merchandise_list.form_list),
)
@ -242,7 +251,7 @@ impl MerchandiseList {
}]);
Ok(sqlx::query_as!(
Self,
"UPDATE
r#"UPDATE
merchandise_lists
SET
form_list = CASE
@ -277,7 +286,13 @@ impl MerchandiseList {
) sub
WHERE
shop_id = $1
RETURNING merchandise_lists.*",
RETURNING
merchandise_lists.id,
merchandise_lists.shop_id,
merchandise_lists.owner_id,
merchandise_lists.created_at,
merchandise_lists.updated_at,
merchandise_lists.form_list as "form_list: Json<Vec<Merchandise>>""#,
shop_id,
mod_name,
&local_form_id.to_string(),

View File

@ -9,12 +9,12 @@ pub mod owner;
pub mod shop;
pub mod transaction;
pub use interior_ref_list::{InteriorRefList, PostedInteriorRefList, UnsavedInteriorRefList};
pub use merchandise_list::{MerchandiseList, PostedMerchandiseList, UnsavedMerchandiseList};
pub use interior_ref_list::{InteriorRefList, PostedInteriorRefList};
pub use merchandise_list::{MerchandiseList, PostedMerchandiseList};
pub use model::{Model, UpdateableModel};
pub use owner::{Owner, PostedOwner, UnsavedOwner};
pub use shop::{PostedShop, Shop, UnsavedShop};
pub use transaction::{PostedTransaction, Transaction, UnsavedTransaction};
pub use owner::{FullPostedOwner, Owner, PostedOwner};
pub use shop::{PostedShop, Shop};
pub use transaction::{PostedTransaction, Transaction};
#[derive(Debug, Eq, PartialEq, Hash, Clone, Deserialize)]
pub enum Order {

View File

@ -24,18 +24,16 @@ pub struct Owner {
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UnsavedOwner {
pub struct PostedOwner {
pub name: String,
#[serde(skip_serializing)]
pub api_key: Uuid,
#[serde(skip_serializing)]
pub ip_address: Option<IpNetwork>,
pub mod_version: i32,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PostedOwner {
pub struct FullPostedOwner {
pub name: String,
pub api_key: Uuid,
pub ip_address: Option<IpNetwork>,
pub mod_version: i32,
}
@ -62,7 +60,7 @@ impl Owner {
#[instrument(level = "debug", skip(owner, db))]
pub async fn create(
owner: UnsavedOwner,
owner: FullPostedOwner,
db: impl Executor<'_, Database = Postgres>,
) -> Result<Self> {
Ok(sqlx::query_as!(

View File

@ -18,13 +18,6 @@ pub struct Shop {
pub updated_at: NaiveDateTime,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UnsavedShop {
pub name: String,
pub owner_id: i32,
pub description: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PostedShop {
pub name: String,
@ -55,7 +48,7 @@ impl Shop {
#[instrument(level = "debug", skip(shop, db))]
pub async fn create(
shop: UnsavedShop,
shop: PostedShop,
db: impl Executor<'_, Database = Postgres>,
) -> Result<Self> {
Ok(sqlx::query_as!(

View File

@ -26,21 +26,6 @@ pub struct Transaction {
pub updated_at: NaiveDateTime,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UnsavedTransaction {
pub shop_id: i32,
pub owner_id: i32,
pub mod_name: String,
pub local_form_id: i32,
pub name: String,
pub form_type: i32,
pub is_food: bool,
pub price: i32,
pub is_sell: bool,
pub quantity: i32,
pub amount: i32,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PostedTransaction {
pub shop_id: i32,
@ -79,7 +64,7 @@ impl Transaction {
#[instrument(level = "debug", skip(db))]
pub async fn create(
transaction: UnsavedTransaction,
transaction: PostedTransaction,
db: impl Executor<'_, Database = Postgres>,
) -> Result<Self> {
Ok(sqlx::query_as!(

View File

@ -1,3 +1,5 @@
use std::borrow::Borrow;
use anyhow::{anyhow, Error};
use http::StatusCode;
use http_api_problem::HttpApiProblem;
@ -31,53 +33,122 @@ pub fn from_anyhow(error: anyhow::Error) -> HttpApiProblem {
Err(error) => error,
};
// TODO: should probably decentralize all this error handling to the places where they are relevant
if let Some(sqlx_error) = error.downcast_ref::<sqlx::error::Error>() {
match sqlx_error {
sqlx::error::Error::RowNotFound => {
return HttpApiProblem::with_title_and_type_from_status(StatusCode::NOT_FOUND)
}
sqlx::error::Error::Database(db_error) => {
let pg_error = db_error.downcast_ref::<sqlx::postgres::PgDatabaseError>();
error!(
"Database error: {}. {}",
pg_error.message(),
pg_error.detail().unwrap_or("")
);
dbg!(&pg_error);
let code = pg_error.code();
dbg!(&code);
if let Some(constraint) = pg_error.constraint() {
dbg!(&constraint);
if code == "23503"
&& (constraint == "shops_owner_id_fkey"
|| constraint == "interior_ref_lists_owner_id_fkey"
|| constraint == "merchandise_lists_owner_id_fkey"
|| constraint == "transactions_owner_id_fkey")
{
// foreign_key_violation
return HttpApiProblem::with_title_and_type_from_status(
StatusCode::BAD_REQUEST,
)
.set_detail("Owner does not exist");
} else if code == "23503"
&& (constraint == "interior_ref_lists_shop_id_fkey"
|| constraint == "merchandise_lists_owner_id_fkey"
|| constraint == "transactions_shop_id_fkey")
{
// foreign_key_violation
return HttpApiProblem::with_title_and_type_from_status(
StatusCode::BAD_REQUEST,
)
.set_detail("Shop does not exist");
} else if code == "23505" && constraint == "owners_api_key_key" {
// unique_violation
return HttpApiProblem::with_title_and_type_from_status(
StatusCode::BAD_REQUEST,
)
.set_detail("Owner with Api-Key already exists");
} else if code == "23505" && constraint == "owners_unique_name_and_api_key" {
// unique_violation
return HttpApiProblem::with_title_and_type_from_status(
StatusCode::BAD_REQUEST,
)
.set_detail("Duplicate owner with same name and Api-Key exists");
} else if code == "23505" && constraint == "shops_unique_name_and_owner_id" {
// unique_violation
return HttpApiProblem::with_title_and_type_from_status(
StatusCode::BAD_REQUEST,
)
.set_detail("Owner already has a shop with that name");
} else if code == "23505" && constraint == "interior_ref_lists_shop_id_key" {
// unique_violation
return HttpApiProblem::with_title_and_type_from_status(
StatusCode::BAD_REQUEST,
)
.set_detail("Interior ref list already exists for that shop");
} else if code == "23505" && constraint == "merchandise_lists_shop_id_key" {
// unique_violation
return HttpApiProblem::with_title_and_type_from_status(
StatusCode::BAD_REQUEST,
)
.set_detail("Merchandise list already exists for that shop");
} else if code == "23514" && constraint == "merchandise_quantity_gt_zero" {
return HttpApiProblem::with_title_and_type_from_status(
StatusCode::BAD_REQUEST,
)
.set_detail("Quantity of merchandise must be greater than zero");
}
}
// Might possibly link sensitive info:
// let mut problem = HttpApiProblem::with_title_and_type_from_status(
// StatusCode::INTERNAL_SERVER_ERROR,
// )
// .set_title("Database Error")
// .set_detail(format!(
// "{}. {}",
// pg_error.message(),
// pg_error.detail().unwrap_or("")
// ));
// problem
// .set_value("code".to_string(), &code.to_string())
// .unwrap();
// return problem;
}
_ => {}
}
}
if let Some(pg_error) = error.downcast_ref::<sqlx::postgres::PgDatabaseError>() {
error!(
"Database error: {}. {}",
pg_error.message(),
pg_error.detail().unwrap_or("")
);
dbg!(&pg_error);
let code = pg_error.code();
dbg!(&code);
if let Some(constraint) = pg_error.constraint() {
dbg!(&constraint);
if code == "23503" && constraint == "shops_owner_id_fkey" {
// foreign_key_violation
return HttpApiProblem::with_title_and_type_from_status(StatusCode::BAD_REQUEST)
.set_detail("Owner does not exist");
} else if code == "23505" && constraint == "owners_api_key_key" {
// unique_violation
return HttpApiProblem::with_title_and_type_from_status(StatusCode::BAD_REQUEST)
.set_detail("Owner with Api-Key already exists");
} else if code == "23505" && constraint == "owners_unique_name_and_api_key" {
// unique_violation
return HttpApiProblem::with_title_and_type_from_status(StatusCode::BAD_REQUEST)
.set_detail("Duplicate owner with same name and Api-Key exists");
} else if code == "23505" && constraint == "shops_unique_name_and_owner_id" {
// unique_violation
return HttpApiProblem::with_title_and_type_from_status(StatusCode::BAD_REQUEST)
.set_detail("Owner already has a shop with that name");
} else if code == "23514" && constraint == "merchandise_quantity_gt_zero" {
return HttpApiProblem::with_title_and_type_from_status(StatusCode::BAD_REQUEST)
.set_detail("Quantity of merchandise must be greater than zero");
if let Some(json_error) = error.downcast_ref::<serde_json::Error>() {
return HttpApiProblem::with_title_and_type_from_status(StatusCode::BAD_REQUEST)
.set_title("Json Body Deserialization Error")
.set_detail(format!("{}", json_error));
}
if let Some(bincode_error) = error.downcast_ref::<bincode::Error>() {
return match bincode_error.borrow() {
bincode::ErrorKind::Io(io_error) => {
HttpApiProblem::with_title_and_type_from_status(StatusCode::BAD_REQUEST)
.set_title("Bincode Body Deserialization Error")
.set_detail(format!("io error ({:?}): {}", io_error.kind(), io_error))
}
}
error => HttpApiProblem::with_title_and_type_from_status(StatusCode::BAD_REQUEST)
.set_title("Bincode Body Deserialization Error")
.set_detail(format!("{}", error)),
};
}
error!("Recovering unhandled error: {:?}", error);
// TODO: this leaks internal info, should not stringify error
HttpApiProblem::new(format!("Internal Server Error: {:?}", error))
.set_status(StatusCode::INTERNAL_SERVER_ERROR)
HttpApiProblem::with_title_and_type_from_status(StatusCode::INTERNAL_SERVER_ERROR)
}
pub async fn unpack_problem(rejection: Rejection) -> Result<impl Reply, Rejection> {