Upgrade to sqlx 0.4 beta master branch

This required a ton of changes including updating error handling and separating out the models into intermediate representations so that fields that are marked non-null in the database are not `Option` in the final model.

The update allows using `query_as!` in `interior_ref_list` and `merchandise_list` and model functions to specify a generic `Executor` param that can take either a db pool connection, transaction, or plain db connection. This should allow me to impl my old `Model` trait again.

Also compile times are magically 20x faster?!?
This commit is contained in:
2020-11-09 00:37:04 -05:00
parent 1a1806ffc3
commit 780f0be433
15 changed files with 771 additions and 521 deletions

View File

@@ -1,19 +1,14 @@
use anyhow::{anyhow, Error, Result};
use anyhow::{Error, Result};
use chrono::prelude::*;
use serde::{Deserialize, Serialize};
use sqlx::types::Json;
use sqlx::PgPool;
use sqlx::{Done, Executor, Postgres};
use tracing::instrument;
use url::Url;
use super::ListParams;
use crate::problem::forbidden_permission;
// sqlx queries for this model need to be `query_as_unchecked!` because `query_as!` does not
// support user-defined types (`ref_list` Json field).
// See for more info: https://github.com/thallada/rust_sqlx_bug/blob/master/src/main.rs
// This may be fixed in sqlx 0.4.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct InteriorRef {
pub base_mod_name: String,
@@ -31,12 +26,26 @@ pub struct InteriorRef {
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct InteriorRefList {
pub id: Option<i32>,
pub id: i32,
pub shop_id: i32,
pub owner_id: i32,
pub ref_list: serde_json::Value,
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,
pub owner_id: Option<i32>,
pub ref_list: Json<Vec<InteriorRef>>,
pub created_at: Option<NaiveDateTime>,
pub updated_at: Option<NaiveDateTime>,
}
impl InteriorRefList {
@@ -44,52 +53,48 @@ impl InteriorRefList {
"interior_ref_list"
}
pub fn pk(&self) -> Option<i32> {
pub fn pk(&self) -> i32 {
self.id
}
pub fn url(&self, api_url: &Url) -> Result<Url> {
if let Some(pk) = self.pk() {
Ok(api_url.join(&format!("{}s/{}", Self::resource_name(), pk))?)
} else {
Err(anyhow!(
"Cannot get URL for {} with no primary key",
Self::resource_name()
))
}
Ok(api_url.join(&format!("{}s/{}", Self::resource_name(), self.pk()))?)
}
// 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: &PgPool, id: i32) -> Result<Self> {
sqlx::query_as_unchecked!(Self, "SELECT * FROM interior_ref_lists WHERE id = $1", id)
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)
}
#[instrument(level = "debug", skip(self, db))]
pub async fn create(self, db: &PgPool) -> Result<Self> {
// TODO:
// * Decide if I'll need to make the same changes to merchandise and transactions
// - answer depends on how many rows of each I expect to insert in one go
// * should probably omit ref_list from response
Ok(sqlx::query_as_unchecked!(
#[instrument(level = "debug", skip(interior_ref_list, db))]
pub async fn create(
interior_ref_list: UnsavedInteriorRefList,
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)
VALUES ($1, $2, $3, now(), now())
RETURNING *",
self.shop_id,
self.owner_id,
self.ref_list,
interior_ref_list.shop_id,
interior_ref_list.owner_id,
serde_json::json!(interior_ref_list.ref_list),
)
.fetch_one(db)
.await?)
}
#[instrument(level = "debug", skip(db))]
pub async fn delete(db: &PgPool, owner_id: i32, id: i32) -> Result<u64> {
pub async fn delete(
db: impl Executor<'_, Database = Postgres> + Copy,
owner_id: i32,
id: i32,
) -> Result<u64> {
let interior_ref_list =
sqlx::query!("SELECT owner_id FROM interior_ref_lists WHERE id = $1", id)
.fetch_one(db)
@@ -98,7 +103,8 @@ impl InteriorRefList {
return Ok(
sqlx::query!("DELETE FROM interior_ref_lists WHERE id = $1", id)
.execute(db)
.await?,
.await?
.rows_affected(),
);
} else {
return Err(forbidden_permission());
@@ -106,9 +112,12 @@ impl InteriorRefList {
}
#[instrument(level = "debug", skip(db))]
pub async fn list(db: &PgPool, list_params: &ListParams) -> Result<Vec<Self>> {
pub async fn list(
db: impl Executor<'_, Database = Postgres>,
list_params: &ListParams,
) -> Result<Vec<Self>> {
let result = if let Some(order_by) = list_params.get_order_by() {
sqlx::query_as_unchecked!(
sqlx::query_as!(
Self,
"SELECT * FROM interior_ref_lists
ORDER BY $1
@@ -121,7 +130,7 @@ impl InteriorRefList {
.fetch_all(db)
.await?
} else {
sqlx::query_as_unchecked!(
sqlx::query_as!(
Self,
"SELECT * FROM interior_ref_lists
LIMIT $1
@@ -135,14 +144,19 @@ impl InteriorRefList {
Ok(result)
}
#[instrument(level = "debug", skip(self, db))]
pub async fn update(self, db: &PgPool, owner_id: i32, id: i32) -> Result<Self> {
let interior_ref_list =
#[instrument(level = "debug", skip(interior_ref_list, db))]
pub async fn update(
interior_ref_list: PostedInteriorRefList,
db: impl Executor<'_, Database = Postgres> + Copy,
owner_id: i32,
id: i32,
) -> Result<Self> {
let existing_interior_ref_list =
sqlx::query!("SELECT owner_id FROM interior_ref_lists WHERE id = $1", id)
.fetch_one(db)
.await?;
if interior_ref_list.owner_id == owner_id {
Ok(sqlx::query_as_unchecked!(
if existing_interior_ref_list.owner_id == owner_id {
Ok(sqlx::query_as!(
Self,
"UPDATE interior_ref_lists SET
ref_list = $2,
@@ -150,7 +164,7 @@ impl InteriorRefList {
WHERE id = $1
RETURNING *",
id,
self.ref_list,
serde_json::json!(interior_ref_list.ref_list),
)
.fetch_one(db)
.await?)
@@ -160,8 +174,11 @@ impl InteriorRefList {
}
#[instrument(level = "debug", skip(db))]
pub async fn get_by_shop_id(db: &PgPool, shop_id: i32) -> Result<Self> {
sqlx::query_as_unchecked!(
pub async fn get_by_shop_id(
db: impl Executor<'_, Database = Postgres>,
shop_id: i32,
) -> Result<Self> {
sqlx::query_as!(
Self,
"SELECT * FROM interior_ref_lists
WHERE shop_id = $1",
@@ -172,16 +189,21 @@ impl InteriorRefList {
.map_err(Error::new)
}
#[instrument(level = "debug", skip(self, db))]
pub async fn update_by_shop_id(self, db: &PgPool, owner_id: i32, shop_id: i32) -> Result<Self> {
let interior_ref_list = sqlx::query!(
#[instrument(level = "debug", skip(interior_ref_list, db))]
pub async fn update_by_shop_id(
interior_ref_list: PostedInteriorRefList,
db: impl Executor<'_, Database = Postgres> + Copy,
owner_id: i32,
shop_id: i32,
) -> Result<Self> {
let existing_interior_ref_list = sqlx::query!(
"SELECT owner_id FROM interior_ref_lists WHERE shop_id = $1",
shop_id
)
.fetch_one(db)
.await?;
if interior_ref_list.owner_id == owner_id {
Ok(sqlx::query_as_unchecked!(
if existing_interior_ref_list.owner_id == owner_id {
Ok(sqlx::query_as!(
Self,
"UPDATE interior_ref_lists SET
ref_list = $2,
@@ -189,7 +211,7 @@ impl InteriorRefList {
WHERE shop_id = $1
RETURNING *",
shop_id,
self.ref_list,
serde_json::json!(interior_ref_list.ref_list),
)
.fetch_one(db)
.await?)

View File

@@ -4,20 +4,14 @@ use http::StatusCode;
use http_api_problem::HttpApiProblem;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::pool::PoolConnection;
use sqlx::types::Json;
use sqlx::{PgConnection, PgPool, Transaction};
use sqlx::{Done, Executor, Postgres};
use tracing::instrument;
use url::Url;
use super::ListParams;
use crate::problem::forbidden_permission;
// sqlx queries for this model need to be `query_as_unchecked!` because `query_as!` does not
// support user-defined types (`form_list` Json field).
// See for more info: https://github.com/thallada/rust_sqlx_bug/blob/master/src/main.rs
// This may be fixed in sqlx 0.4.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Merchandise {
pub mod_name: String,
@@ -31,12 +25,26 @@ pub struct Merchandise {
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct MerchandiseList {
pub id: Option<i32>,
pub id: i32,
pub shop_id: i32,
pub owner_id: i32,
pub form_list: serde_json::Value,
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,
pub owner_id: Option<i32>,
pub form_list: Json<Vec<Merchandise>>,
pub created_at: Option<NaiveDateTime>,
pub updated_at: Option<NaiveDateTime>,
}
impl MerchandiseList {
@@ -44,48 +52,48 @@ impl MerchandiseList {
"merchandise_list"
}
pub fn pk(&self) -> Option<i32> {
pub fn pk(&self) -> i32 {
self.id
}
pub fn url(&self, api_url: &Url) -> Result<Url> {
if let Some(pk) = self.pk() {
Ok(api_url.join(&format!("{}s/{}", Self::resource_name(), pk))?)
} else {
Err(anyhow!(
"Cannot get URL for {} with no primary key",
Self::resource_name()
))
}
Ok(api_url.join(&format!("{}s/{}", Self::resource_name(), self.pk()))?)
}
// 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: &PgPool, id: i32) -> Result<Self> {
sqlx::query_as_unchecked!(Self, "SELECT * FROM merchandise_lists WHERE id = $1", id)
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)
}
#[instrument(level = "debug", skip(self, db))]
pub async fn create(self, db: &PgPool) -> Result<Self> {
Ok(sqlx::query_as_unchecked!(
#[instrument(level = "debug", skip(merchandise_list, db))]
pub async fn create(
merchandise_list: UnsavedMerchandiseList,
db: impl Executor<'_, Database = Postgres>,
) -> Result<Self> {
Ok(sqlx::query_as!(
Self,
"INSERT INTO merchandise_lists
(shop_id, owner_id, form_list, created_at, updated_at)
VALUES ($1, $2, $3, now(), now())
RETURNING *",
self.shop_id,
self.owner_id,
self.form_list,
merchandise_list.shop_id,
merchandise_list.owner_id,
serde_json::json!(merchandise_list.form_list),
)
.fetch_one(db)
.await?)
}
#[instrument(level = "debug", skip(db))]
pub async fn delete(db: &PgPool, owner_id: i32, id: i32) -> Result<u64> {
pub async fn delete(
db: impl Executor<'_, Database = Postgres> + Copy,
owner_id: i32,
id: i32,
) -> Result<u64> {
let merchandise_list =
sqlx::query!("SELECT owner_id FROM merchandise_lists WHERE id = $1", id)
.fetch_one(db)
@@ -94,7 +102,8 @@ impl MerchandiseList {
return Ok(
sqlx::query!("DELETE FROM merchandise_lists WHERE id = $1", id)
.execute(db)
.await?,
.await?
.rows_affected(),
);
} else {
return Err(forbidden_permission());
@@ -102,9 +111,12 @@ impl MerchandiseList {
}
#[instrument(level = "debug", skip(db))]
pub async fn list(db: &PgPool, list_params: &ListParams) -> Result<Vec<Self>> {
pub async fn list(
db: impl Executor<'_, Database = Postgres>,
list_params: &ListParams,
) -> Result<Vec<Self>> {
let result = if let Some(order_by) = list_params.get_order_by() {
sqlx::query_as_unchecked!(
sqlx::query_as!(
Self,
"SELECT * FROM merchandise_lists
ORDER BY $1
@@ -117,7 +129,7 @@ impl MerchandiseList {
.fetch_all(db)
.await?
} else {
sqlx::query_as_unchecked!(
sqlx::query_as!(
Self,
"SELECT * FROM merchandise_lists
LIMIT $1
@@ -131,14 +143,19 @@ impl MerchandiseList {
Ok(result)
}
#[instrument(level = "debug", skip(self, db))]
pub async fn update(self, db: &PgPool, owner_id: i32, id: i32) -> Result<Self> {
let merchandise_list =
#[instrument(level = "debug", skip(merchandise_list, db))]
pub async fn update(
merchandise_list: PostedMerchandiseList,
db: impl Executor<'_, Database = Postgres> + Copy,
owner_id: i32,
id: i32,
) -> Result<Self> {
let existing_merchandise_list =
sqlx::query!("SELECT owner_id FROM merchandise_lists WHERE id = $1", id)
.fetch_one(db)
.await?;
if merchandise_list.owner_id == owner_id {
Ok(sqlx::query_as_unchecked!(
if existing_merchandise_list.owner_id == owner_id {
Ok(sqlx::query_as!(
Self,
"UPDATE merchandise_lists SET
form_list = $2,
@@ -146,7 +163,7 @@ impl MerchandiseList {
WHERE id = $1
RETURNING *",
id,
self.form_list,
serde_json::json!(merchandise_list.form_list),
)
.fetch_one(db)
.await?)
@@ -156,8 +173,11 @@ impl MerchandiseList {
}
#[instrument(level = "debug", skip(db))]
pub async fn get_by_shop_id(db: &PgPool, shop_id: i32) -> Result<Self> {
sqlx::query_as_unchecked!(
pub async fn get_by_shop_id(
db: impl Executor<'_, Database = Postgres>,
shop_id: i32,
) -> Result<Self> {
sqlx::query_as!(
Self,
"SELECT * FROM merchandise_lists
WHERE shop_id = $1",
@@ -168,16 +188,21 @@ impl MerchandiseList {
.map_err(Error::new)
}
#[instrument(level = "debug", skip(self, db))]
pub async fn update_by_shop_id(self, db: &PgPool, owner_id: i32, shop_id: i32) -> Result<Self> {
let merchandise_list = sqlx::query!(
#[instrument(level = "debug", skip(merchandise_list, db))]
pub async fn update_by_shop_id(
merchandise_list: PostedMerchandiseList,
db: impl Executor<'_, Database = Postgres> + Copy,
owner_id: i32,
shop_id: i32,
) -> Result<Self> {
let existing_merchandise_list = sqlx::query!(
"SELECT owner_id FROM merchandise_lists WHERE shop_id = $1",
shop_id
)
.fetch_one(db)
.await?;
if merchandise_list.owner_id == owner_id {
Ok(sqlx::query_as_unchecked!(
if existing_merchandise_list.owner_id == owner_id {
Ok(sqlx::query_as!(
Self,
"UPDATE merchandise_lists SET
form_list = $2,
@@ -185,7 +210,7 @@ impl MerchandiseList {
WHERE shop_id = $1
RETURNING *",
shop_id,
self.form_list,
serde_json::json!(merchandise_list.form_list),
)
.fetch_one(db)
.await?)
@@ -196,7 +221,7 @@ impl MerchandiseList {
#[instrument(level = "debug", skip(db))]
pub async fn update_merchandise_quantity(
db: &mut Transaction<PoolConnection<PgConnection>>,
db: impl Executor<'_, Database = Postgres>,
shop_id: i32,
mod_name: &str,
local_form_id: i32,
@@ -215,7 +240,7 @@ impl MerchandiseList {
"is_food": is_food,
"price": price,
}]);
Ok(sqlx::query_as_unchecked!(
Ok(sqlx::query_as!(
Self,
"UPDATE
merchandise_lists
@@ -255,7 +280,7 @@ impl MerchandiseList {
RETURNING merchandise_lists.*",
shop_id,
mod_name,
local_form_id,
&local_form_id.to_string(),
quantity_delta,
add_item,
)
@@ -263,10 +288,10 @@ impl MerchandiseList {
.await
.map_err(|error| {
let anyhow_error = anyhow!(error);
if let Some(sqlx::error::Error::Database(db_error)) =
anyhow_error.downcast_ref::<sqlx::error::Error>()
if let Some(db_error) =
anyhow_error.downcast_ref::<sqlx::postgres::PgDatabaseError>()
{
if db_error.code() == Some("23502") && db_error.column_name() == Some("form_list") {
if db_error.code() == "23502" && db_error.column() == Some("form_list") {
return anyhow!(HttpApiProblem::with_title_and_type_from_status(
StatusCode::NOT_FOUND
)

View File

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

View File

@@ -1,8 +1,8 @@
use anyhow::{anyhow, Error, Result};
use anyhow::{Error, Result};
use chrono::prelude::*;
use ipnetwork::IpNetwork;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use sqlx::{Done, Executor, Postgres};
use tracing::instrument;
use url::Url;
use uuid::Uuid;
@@ -12,15 +12,31 @@ use crate::problem::forbidden_permission;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Owner {
pub id: Option<i32>,
pub id: i32,
pub name: String,
#[serde(skip_serializing)]
pub api_key: Option<Uuid>,
pub api_key: Uuid,
#[serde(skip_serializing)]
pub ip_address: Option<IpNetwork>,
pub mod_version: i32,
pub created_at: Option<NaiveDateTime>,
pub updated_at: Option<NaiveDateTime>,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UnsavedOwner {
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 name: String,
pub mod_version: i32,
}
impl Owner {
@@ -28,62 +44,66 @@ impl Owner {
"owner"
}
pub fn pk(&self) -> Option<i32> {
pub fn pk(&self) -> i32 {
self.id
}
pub fn url(&self, api_url: &Url) -> Result<Url> {
if let Some(pk) = self.pk() {
Ok(api_url.join(&format!("{}s/{}", Self::resource_name(), pk))?)
} else {
Err(anyhow!(
"Cannot get URL for {} with no primary key",
Self::resource_name()
))
}
Ok(api_url.join(&format!("{}s/{}", Self::resource_name(), self.pk()))?)
}
#[instrument(level = "debug", skip(db))]
pub async fn get(db: &PgPool, id: i32) -> Result<Self> {
pub async fn get(db: impl Executor<'_, Database = Postgres>, id: i32) -> Result<Self> {
sqlx::query_as!(Self, "SELECT * FROM owners WHERE id = $1", id)
.fetch_one(db)
.await
.map_err(Error::new)
}
#[instrument(level = "debug", skip(self, db))]
pub async fn create(self, db: &PgPool) -> Result<Self> {
#[instrument(level = "debug", skip(owner, db))]
pub async fn create(
owner: UnsavedOwner,
db: impl Executor<'_, Database = Postgres>,
) -> Result<Self> {
Ok(sqlx::query_as!(
Self,
"INSERT INTO owners
(name, api_key, ip_address, mod_version, created_at, updated_at)
VALUES ($1, $2, $3, $4, now(), now())
RETURNING *",
self.name,
self.api_key,
self.ip_address,
self.mod_version,
owner.name,
owner.api_key,
owner.ip_address,
owner.mod_version,
)
.fetch_one(db)
.await?)
}
#[instrument(level = "debug", skip(db))]
pub async fn delete(db: &PgPool, owner_id: i32, id: i32) -> Result<u64> {
pub async fn delete(
db: impl Executor<'_, Database = Postgres> + Copy,
owner_id: i32,
id: i32,
) -> Result<u64> {
let owner = sqlx::query!("SELECT id FROM owners WHERE id = $1", id)
.fetch_one(db)
.await?;
if owner.id == owner_id {
Ok(sqlx::query!("DELETE FROM owners WHERE id = $1", id)
.execute(db)
.await?)
.await?
.rows_affected())
} else {
return Err(forbidden_permission());
}
}
#[instrument(level = "debug", skip(db))]
pub async fn list(db: &PgPool, list_params: &ListParams) -> Result<Vec<Self>> {
pub async fn list(
db: impl Executor<'_, Database = Postgres>,
list_params: &ListParams,
) -> Result<Vec<Self>> {
let result = if let Some(order_by) = list_params.get_order_by() {
sqlx::query_as!(
Self,
@@ -112,12 +132,17 @@ impl Owner {
Ok(result)
}
#[instrument(level = "debug", skip(self, db))]
pub async fn update(self, db: &PgPool, owner_id: i32, id: i32) -> Result<Self> {
let owner = sqlx::query!("SELECT id FROM owners WHERE id = $1", id)
#[instrument(level = "debug", skip(owner, db))]
pub async fn update(
owner: PostedOwner,
db: impl Executor<'_, Database = Postgres> + Copy,
owner_id: i32,
id: i32,
) -> Result<Self> {
let existing_owner = sqlx::query!("SELECT id FROM owners WHERE id = $1", id)
.fetch_one(db)
.await?;
if owner.id == owner_id {
if existing_owner.id == owner_id {
Ok(sqlx::query_as!(
Self,
"UPDATE owners SET
@@ -127,8 +152,8 @@ impl Owner {
WHERE id = $1
RETURNING *",
id,
self.name,
self.mod_version,
owner.name,
owner.mod_version,
)
.fetch_one(db)
.await?)

View File

@@ -1,7 +1,7 @@
use anyhow::{anyhow, Error, Result};
use anyhow::{Error, Result};
use chrono::prelude::*;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use sqlx::{Done, Executor, Postgres};
use tracing::instrument;
use url::Url;
@@ -10,17 +10,26 @@ use crate::problem::forbidden_permission;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Shop {
pub id: Option<i32>,
pub id: i32,
pub name: String,
pub owner_id: i32,
pub description: Option<String>,
pub created_at: NaiveDateTime,
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,
pub owner_id: Option<i32>,
pub description: String,
// removing these until I figure out the plan for buying and selling
// pub is_not_sell_buy: bool,
// pub sell_buy_list_id: i32,
// pub vendor_id: i32,
// pub vendor_gold: i32,
pub created_at: Option<NaiveDateTime>,
pub updated_at: Option<NaiveDateTime>,
pub description: Option<String>,
}
impl Shop {
@@ -28,61 +37,65 @@ impl Shop {
"shop"
}
pub fn pk(&self) -> Option<i32> {
pub fn pk(&self) -> i32 {
self.id
}
pub fn url(&self, api_url: &Url) -> Result<Url> {
if let Some(pk) = self.pk() {
Ok(api_url.join(&format!("{}s/{}", Self::resource_name(), pk))?)
} else {
Err(anyhow!(
"Cannot get URL for {} with no primary key",
Self::resource_name()
))
}
Ok(api_url.join(&format!("{}s/{}", Self::resource_name(), self.pk()))?)
}
#[instrument(level = "debug", skip(db))]
pub async fn get(db: &PgPool, id: i32) -> Result<Self> {
pub async fn get(db: impl Executor<'_, Database = Postgres>, id: i32) -> Result<Self> {
sqlx::query_as!(Self, "SELECT * FROM shops WHERE id = $1", id)
.fetch_one(db)
.await
.map_err(Error::new)
}
#[instrument(level = "debug", skip(self, db))]
pub async fn create(self, db: &PgPool) -> Result<Self> {
#[instrument(level = "debug", skip(shop, db))]
pub async fn create(
shop: UnsavedShop,
db: impl Executor<'_, Database = Postgres>,
) -> Result<Self> {
Ok(sqlx::query_as!(
Self,
"INSERT INTO shops
(name, owner_id, description, created_at, updated_at)
VALUES ($1, $2, $3, now(), now())
RETURNING *",
self.name,
self.owner_id,
self.description,
shop.name,
shop.owner_id,
shop.description,
)
.fetch_one(db)
.await?)
}
#[instrument(level = "debug", skip(db))]
pub async fn delete(db: &PgPool, owner_id: i32, id: i32) -> Result<u64> {
pub async fn delete(
db: impl Executor<'_, Database = Postgres> + Copy,
owner_id: i32,
id: i32,
) -> Result<u64> {
let shop = sqlx::query!("SELECT owner_id FROM shops WHERE id = $1", id)
.fetch_one(db)
.await?;
if shop.owner_id == owner_id {
return Ok(sqlx::query!("DELETE FROM shops WHERE shops.id = $1", id)
.execute(db)
.await?);
.await?
.rows_affected());
} else {
return Err(forbidden_permission());
}
}
#[instrument(level = "debug", skip(db))]
pub async fn list(db: &PgPool, list_params: &ListParams) -> Result<Vec<Self>> {
pub async fn list(
db: impl Executor<'_, Database = Postgres>,
list_params: &ListParams,
) -> Result<Vec<Self>> {
let result = if let Some(order_by) = list_params.get_order_by() {
sqlx::query_as!(
Self,
@@ -111,12 +124,17 @@ impl Shop {
Ok(result)
}
#[instrument(level = "debug", skip(self, db))]
pub async fn update(self, db: &PgPool, owner_id: i32, id: i32) -> Result<Self> {
let shop = sqlx::query!("SELECT owner_id FROM shops WHERE id = $1", id)
#[instrument(level = "debug", skip(shop, db))]
pub async fn update(
shop: PostedShop,
db: impl Executor<'_, Database = Postgres> + Copy,
owner_id: i32,
id: i32,
) -> Result<Self> {
let existing_shop = sqlx::query!("SELECT owner_id FROM shops WHERE id = $1", id)
.fetch_one(db)
.await?;
if shop.owner_id == owner_id {
if existing_shop.owner_id == owner_id {
Ok(sqlx::query_as!(
Self,
"UPDATE shops SET
@@ -127,9 +145,9 @@ impl Shop {
WHERE id = $1
RETURNING *",
id,
self.name,
self.owner_id,
self.description,
shop.name,
shop.owner_id,
shop.description,
)
.fetch_one(db)
.await?)

View File

@@ -1,8 +1,7 @@
use anyhow::{anyhow, Error, Result};
use anyhow::{Error, Result};
use chrono::prelude::*;
use serde::{Deserialize, Serialize};
use sqlx::pool::PoolConnection;
use sqlx::{PgConnection, PgPool};
use sqlx::{Done, Executor, Postgres};
use tracing::instrument;
use url::Url;
@@ -11,7 +10,39 @@ use crate::problem::forbidden_permission;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Transaction {
pub id: Option<i32>,
pub id: i32,
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,
pub created_at: NaiveDateTime,
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,
pub owner_id: Option<i32>,
pub mod_name: String,
@@ -23,8 +54,6 @@ pub struct Transaction {
pub is_sell: bool,
pub quantity: i32,
pub amount: i32,
pub created_at: Option<NaiveDateTime>,
pub updated_at: Option<NaiveDateTime>,
}
impl Transaction {
@@ -32,23 +61,16 @@ impl Transaction {
"transaction"
}
pub fn pk(&self) -> Option<i32> {
pub fn pk(&self) -> i32 {
self.id
}
pub fn url(&self, api_url: &Url) -> Result<Url> {
if let Some(pk) = self.pk() {
Ok(api_url.join(&format!("{}s/{}", Self::resource_name(), pk))?)
} else {
Err(anyhow!(
"Cannot get URL for {} with no primary key",
Self::resource_name()
))
}
Ok(api_url.join(&format!("{}s/{}", Self::resource_name(), self.pk()))?)
}
#[instrument(level = "debug", skip(db))]
pub async fn get(db: &PgPool, id: i32) -> Result<Self> {
pub async fn get(db: impl Executor<'_, Database = Postgres>, id: i32) -> Result<Self> {
sqlx::query_as!(Self, "SELECT * FROM transactions WHERE id = $1", id)
.fetch_one(db)
.await
@@ -57,8 +79,8 @@ impl Transaction {
#[instrument(level = "debug", skip(db))]
pub async fn create(
self,
db: &mut sqlx::Transaction<PoolConnection<PgConnection>>,
transaction: UnsavedTransaction,
db: impl Executor<'_, Database = Postgres>,
) -> Result<Self> {
Ok(sqlx::query_as!(
Self,
@@ -67,38 +89,46 @@ impl Transaction {
is_sell, quantity, amount, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, now(), now())
RETURNING *",
self.shop_id,
self.owner_id,
self.mod_name,
self.local_form_id,
self.name,
self.form_type,
self.is_food,
self.price,
self.is_sell,
self.quantity,
self.amount,
transaction.shop_id,
transaction.owner_id,
transaction.mod_name,
transaction.local_form_id,
transaction.name,
transaction.form_type,
transaction.is_food,
transaction.price,
transaction.is_sell,
transaction.quantity,
transaction.amount,
)
.fetch_one(db)
.await?)
}
#[instrument(level = "debug", skip(db))]
pub async fn delete(db: &PgPool, owner_id: i32, id: i32) -> Result<u64> {
pub async fn delete(
db: impl Executor<'_, Database = Postgres> + Copy,
owner_id: i32,
id: i32,
) -> Result<u64> {
let transaction = sqlx::query!("SELECT owner_id FROM transactions WHERE id = $1", id)
.fetch_one(db)
.await?;
if transaction.owner_id == owner_id {
return Ok(sqlx::query!("DELETE FROM transactions WHERE id = $1", id)
.execute(db)
.await?);
.await?
.rows_affected());
} else {
return Err(forbidden_permission());
}
}
#[instrument(level = "debug", skip(db))]
pub async fn list(db: &PgPool, list_params: &ListParams) -> Result<Vec<Self>> {
pub async fn list(
db: impl Executor<'_, Database = Postgres>,
list_params: &ListParams,
) -> Result<Vec<Self>> {
let result = if let Some(order_by) = list_params.get_order_by() {
sqlx::query_as!(
Self,
@@ -129,7 +159,7 @@ impl Transaction {
#[instrument(level = "debug", skip(db))]
pub async fn list_by_shop_id(
db: &PgPool,
db: impl Executor<'_, Database = Postgres>,
shop_id: i32,
list_params: &ListParams,
) -> Result<Vec<Self>> {