Working create_transaction endpoint

Creates the transaction record and updates the merchandise quantity in one db transaction.

Managed to do the merchandise update in one UPDATE query, but the error that's thrown when an item to buy is not found is pretty confusing, so I convert it to a 404.

I also added some DB indexes.
This commit is contained in:
Tyler Hallada 2020-10-31 20:34:20 -04:00
parent c87c35021e
commit 08c8dcb07b
16 changed files with 249 additions and 167 deletions

View File

@ -34,15 +34,23 @@ CREATE TABLE "merchandise_lists" (
"created_at" timestamp(3) NOT NULL,
"updated_at" timestamp(3) NOT NULL
);
CREATE INDEX "merchandise_lists_mod_name_and_local_form_id" ON "merchandise_lists" USING GIN (form_list jsonb_path_ops);
CREATE TABLE "transactions" (
"id" SERIAL PRIMARY KEY NOT NULL,
"shop_id" INTEGER REFERENCES "shops"(id) NOT NULL UNIQUE,
"shop_id" INTEGER REFERENCES "shops"(id) NOT NULL,
"owner_id" INTEGER REFERENCES "owners"(id) NOT NULL,
"mod_name" VARCHAR(260) NOT NULL,
"local_form_id" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"form_type" INTEGER NOT NULL,
"is_food" BOOLEAN NOT NULL,
"price" INTEGER NOT NULL,
"is_sell" BOOLEAN NOT NULL,
"quantity" INTEGER NOT NULL,
"amount" INTEGER NOT NULL,
"created_at" timestamp(3) NOT NULL,
"updated_at" timestamp(3) NOT NULL
);
CREATE INDEX "transactions_shop_id" ON "transactions" ("shop_id");
CREATE INDEX "transactions_owner_id" ON "transactions" ("owner_id");
CREATE INDEX "transactions_mod_name_and_local_form_id" ON "transactions" ("mod_name", "local_form_id");

View File

@ -2,4 +2,5 @@ DROP TABLE owners CASCADE;
DROP TABLE shops CASCADE;
DROP TABLE interior_ref_lists CASCADE;
DROP TABLE merchandise_lists CASCADE;
DROP TABLE transactions CASCADE;
DROP TABLE refinery_schema_history CASCADE;

View File

@ -4,7 +4,7 @@ use uuid::Uuid;
use warp::reply::{json, with_header, with_status};
use warp::{Rejection, Reply};
use crate::models::{InteriorRefList, ListParams, Model, UpdateableModel};
use crate::models::{InteriorRefList, ListParams};
use crate::problem::reject_anyhow;
use crate::Environment;

View File

@ -4,7 +4,7 @@ use uuid::Uuid;
use warp::reply::{json, with_header, with_status};
use warp::{Rejection, Reply};
use crate::models::{ListParams, MerchandiseList, MerchandiseParams, Model, UpdateableModel};
use crate::models::{ListParams, MerchandiseList};
use crate::problem::reject_anyhow;
use crate::Environment;
@ -169,42 +169,3 @@ pub async fn delete(
env.caches.list_merchandise_lists.clear().await;
Ok(StatusCode::NO_CONTENT)
}
pub async fn buy_merchandise(
shop_id: i32,
merchandise_params: MerchandiseParams,
api_key: Option<Uuid>,
env: Environment,
) -> Result<impl Reply, Rejection> {
let _owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
// TODO: create transaction
let updated_merchandise_list = MerchandiseList::update_merchandise_quantity(
&env.db,
shop_id,
&(merchandise_params.mod_name),
merchandise_params.local_form_id,
merchandise_params.quantity_delta,
)
.await
.map_err(reject_anyhow)?;
let url = updated_merchandise_list
.url(&env.api_url)
.map_err(reject_anyhow)?;
let reply = json(&updated_merchandise_list);
let reply = with_header(reply, "Location", url.as_str());
let reply = with_status(reply, StatusCode::CREATED);
env.caches
.merchandise_list
.delete_response(
updated_merchandise_list
.id
.expect("saved merchandise_list has no id"),
)
.await;
env.caches
.merchandise_list_by_shop_id
.delete_response(updated_merchandise_list.shop_id)
.await;
env.caches.list_merchandise_lists.clear().await;
Ok(reply)
}

View File

@ -6,7 +6,7 @@ use uuid::Uuid;
use warp::reply::{json, with_header, with_status};
use warp::{Rejection, Reply};
use crate::models::{ListParams, Model, Owner, UpdateableModel};
use crate::models::{ListParams, Owner};
use crate::problem::{reject_anyhow, unauthorized_no_api_key};
use crate::Environment;

View File

@ -5,7 +5,7 @@ use uuid::Uuid;
use warp::reply::{json, with_header, with_status};
use warp::{Rejection, Reply};
use crate::models::{InteriorRefList, ListParams, MerchandiseList, Model, Shop, UpdateableModel};
use crate::models::{InteriorRefList, ListParams, MerchandiseList, Shop};
use crate::problem::reject_anyhow;
use crate::Environment;

View File

@ -1,10 +1,10 @@
use anyhow::Result;
use anyhow::{anyhow, Result};
use http::StatusCode;
use uuid::Uuid;
use warp::reply::{json, with_header, with_status};
use warp::{Rejection, Reply};
use crate::models::{ListParams, Model, Transaction};
use crate::models::{ListParams, MerchandiseList, Transaction};
use crate::problem::reject_anyhow;
use crate::Environment;
@ -60,10 +60,35 @@ pub async fn create(
owner_id: Some(owner_id),
..transaction
};
let mut tx = env
.db
.begin()
.await
.map_err(|error| reject_anyhow(anyhow!(error)))?;
let saved_transaction = transaction_with_owner_id
.create(&env.db)
.create(&mut tx)
.await
.map_err(reject_anyhow)?;
let quantity_delta = match transaction.is_sell {
true => transaction.quantity,
false => transaction.quantity * -1,
};
let updated_merchandise_list = MerchandiseList::update_merchandise_quantity(
&mut tx,
saved_transaction.shop_id,
&(saved_transaction.mod_name),
saved_transaction.local_form_id,
&(saved_transaction.name),
saved_transaction.form_type,
saved_transaction.is_food,
saved_transaction.price,
quantity_delta,
)
.await
.map_err(reject_anyhow)?;
tx.commit()
.await
.map_err(|error| reject_anyhow(anyhow!(error)))?;
let url = saved_transaction.url(&env.api_url).map_err(reject_anyhow)?;
let reply = json(&saved_transaction);
let reply = with_header(reply, "Location", url.as_str());
@ -71,6 +96,19 @@ pub async fn create(
// TODO: will this make these caches effectively useless?
env.caches.list_transactions.clear().await;
env.caches.list_transactions_by_shop_id.clear().await;
env.caches
.merchandise_list
.delete_response(
updated_merchandise_list
.id
.expect("saved merchandise_list has no id"),
)
.await;
env.caches
.merchandise_list_by_shop_id
.delete_response(updated_merchandise_list.shop_id)
.await;
env.caches.list_merchandise_lists.clear().await;
Ok(reply)
}
@ -80,7 +118,6 @@ pub async fn delete(
env: Environment,
) -> Result<impl Reply, Rejection> {
let owner_id = authenticate(&env, api_key).await.map_err(reject_anyhow)?;
let transaction = Transaction::get(&env.db, id).await.map_err(reject_anyhow)?;
Transaction::delete(&env.db, owner_id, id)
.await
.map_err(reject_anyhow)?;

View File

@ -19,9 +19,7 @@ mod models;
mod problem;
use caches::Caches;
use models::{
InteriorRefList, ListParams, MerchandiseList, MerchandiseParams, Owner, Shop, Transaction,
};
use models::{InteriorRefList, ListParams, MerchandiseList, Owner, Shop, Transaction};
#[derive(Debug, Clone)]
pub struct Environment {
@ -273,16 +271,6 @@ async fn main() -> Result<()> {
.and(with_env(env.clone()))
.and_then(handlers::merchandise_list::get_by_shop_id),
);
let buy_merchandise_handler = warp::path("shops").and(
warp::path::param()
.and(warp::path("buy_merchandise"))
.and(warp::path::end())
.and(warp::post())
.and(warp::query::<MerchandiseParams>())
.and(warp::header::optional("api-key"))
.and(with_env(env.clone()))
.and_then(handlers::merchandise_list::buy_merchandise),
);
let get_transaction_handler = warp::path("transactions").and(
warp::path::param()
.and(warp::path::end())
@ -341,7 +329,6 @@ async fn main() -> Result<()> {
update_interior_ref_list_by_shop_id_handler,
update_merchandise_list_by_shop_id_handler,
list_transactions_by_shop_id_handler,
buy_merchandise_handler,
get_interior_ref_list_handler,
delete_interior_ref_list_handler,
update_interior_ref_list_handler,

View File

@ -1,13 +1,12 @@
use anyhow::{Error, Result};
use async_trait::async_trait;
use anyhow::{anyhow, Error, Result};
use chrono::prelude::*;
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPool;
use sqlx::types::Json;
use sqlx::PgPool;
use tracing::instrument;
use url::Url;
use super::ListParams;
use super::{Model, UpdateableModel};
use crate::problem::forbidden_permission;
// sqlx queries for this model need to be `query_as_unchecked!` because `query_as!` does not
@ -40,19 +39,29 @@ pub struct InteriorRefList {
pub updated_at: Option<NaiveDateTime>,
}
#[async_trait]
impl Model for InteriorRefList {
fn resource_name() -> &'static str {
impl InteriorRefList {
pub fn resource_name() -> &'static str {
"interior_ref_list"
}
fn pk(&self) -> Option<i32> {
pub fn pk(&self) -> Option<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()
))
}
}
// 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))]
async fn get(db: &PgPool, id: i32) -> Result<Self> {
pub async fn get(db: &PgPool, id: i32) -> Result<Self> {
sqlx::query_as_unchecked!(Self, "SELECT * FROM interior_ref_lists WHERE id = $1", id)
.fetch_one(db)
.await
@ -60,7 +69,7 @@ impl Model for InteriorRefList {
}
#[instrument(level = "debug", skip(self, db))]
async fn create(self, db: &PgPool) -> Result<Self> {
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
@ -80,7 +89,7 @@ impl Model for InteriorRefList {
}
#[instrument(level = "debug", skip(db))]
async fn delete(db: &PgPool, owner_id: i32, id: i32) -> Result<u64> {
pub async fn delete(db: &PgPool, 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)
@ -97,7 +106,7 @@ impl Model for InteriorRefList {
}
#[instrument(level = "debug", skip(db))]
async fn list(db: &PgPool, list_params: &ListParams) -> Result<Vec<Self>> {
pub async fn list(db: &PgPool, list_params: &ListParams) -> Result<Vec<Self>> {
let result = if let Some(order_by) = list_params.get_order_by() {
sqlx::query_as_unchecked!(
Self,
@ -125,12 +134,9 @@ impl Model for InteriorRefList {
};
Ok(result)
}
}
#[async_trait]
impl UpdateableModel for InteriorRefList {
#[instrument(level = "debug", skip(self, db))]
async fn update(self, db: &PgPool, owner_id: i32, id: i32) -> Result<Self> {
pub async fn update(self, db: &PgPool, owner_id: i32, id: i32) -> Result<Self> {
let interior_ref_list =
sqlx::query!("SELECT owner_id FROM interior_ref_lists WHERE id = $1", id)
.fetch_one(db)
@ -152,9 +158,7 @@ impl UpdateableModel for InteriorRefList {
return Err(forbidden_permission());
}
}
}
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!(

View File

@ -1,13 +1,16 @@
use anyhow::{Error, Result};
use async_trait::async_trait;
use anyhow::{anyhow, Context, Error, Result};
use chrono::prelude::*;
use http::StatusCode;
use http_api_problem::HttpApiProblem;
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPool;
use serde_json::json;
use sqlx::pool::PoolConnection;
use sqlx::types::Json;
use sqlx::{PgConnection, PgPool, Transaction};
use tracing::instrument;
use url::Url;
use super::ListParams;
use super::{Model, UpdateableModel};
use crate::problem::forbidden_permission;
// sqlx queries for this model need to be `query_as_unchecked!` because `query_as!` does not
@ -36,26 +39,29 @@ pub struct MerchandiseList {
pub updated_at: Option<NaiveDateTime>,
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Deserialize)]
pub struct MerchandiseParams {
pub mod_name: String,
pub local_form_id: i32,
pub quantity_delta: i32,
}
#[async_trait]
impl Model for MerchandiseList {
fn resource_name() -> &'static str {
impl MerchandiseList {
pub fn resource_name() -> &'static str {
"merchandise_list"
}
fn pk(&self) -> Option<i32> {
pub fn pk(&self) -> Option<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()
))
}
}
// 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))]
async fn get(db: &PgPool, id: i32) -> Result<Self> {
pub async fn get(db: &PgPool, id: i32) -> Result<Self> {
sqlx::query_as_unchecked!(Self, "SELECT * FROM merchandise_lists WHERE id = $1", id)
.fetch_one(db)
.await
@ -63,7 +69,7 @@ impl Model for MerchandiseList {
}
#[instrument(level = "debug", skip(self, db))]
async fn create(self, db: &PgPool) -> Result<Self> {
pub async fn create(self, db: &PgPool) -> Result<Self> {
Ok(sqlx::query_as_unchecked!(
Self,
"INSERT INTO merchandise_lists
@ -79,7 +85,7 @@ impl Model for MerchandiseList {
}
#[instrument(level = "debug", skip(db))]
async fn delete(db: &PgPool, owner_id: i32, id: i32) -> Result<u64> {
pub async fn delete(db: &PgPool, 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)
@ -96,7 +102,7 @@ impl Model for MerchandiseList {
}
#[instrument(level = "debug", skip(db))]
async fn list(db: &PgPool, list_params: &ListParams) -> Result<Vec<Self>> {
pub async fn list(db: &PgPool, list_params: &ListParams) -> Result<Vec<Self>> {
let result = if let Some(order_by) = list_params.get_order_by() {
sqlx::query_as_unchecked!(
Self,
@ -124,12 +130,9 @@ impl Model for MerchandiseList {
};
Ok(result)
}
}
#[async_trait]
impl UpdateableModel for MerchandiseList {
#[instrument(level = "debug", skip(self, db))]
async fn update(self, db: &PgPool, owner_id: i32, id: i32) -> Result<Self> {
pub async fn update(self, db: &PgPool, owner_id: i32, id: i32) -> Result<Self> {
let merchandise_list =
sqlx::query!("SELECT owner_id FROM merchandise_lists WHERE id = $1", id)
.fetch_one(db)
@ -151,9 +154,7 @@ impl UpdateableModel for MerchandiseList {
return Err(forbidden_permission());
}
}
}
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!(
@ -195,26 +196,43 @@ impl MerchandiseList {
#[instrument(level = "debug", skip(db))]
pub async fn update_merchandise_quantity(
db: &PgPool,
db: &mut Transaction<PoolConnection<PgConnection>>,
shop_id: i32,
mod_name: &str,
local_form_id: i32,
name: &str,
form_type: i32,
is_food: bool,
price: i32,
quantity_delta: i32,
) -> Result<Self> {
let add_item = json!([{
"mod_name": mod_name,
"local_form_id": local_form_id,
"name": name,
"quantity": quantity_delta,
"form_type": form_type,
"is_food": is_food,
"price": price,
}]);
Ok(sqlx::query_as_unchecked!(
Self,
"UPDATE
merchandise_lists
SET
form_list = CASE
WHEN quantity::int + $4 = 0
WHEN elem_index IS NULL AND quantity IS NULL AND $4 > 0
THEN form_list || $5
WHEN elem_index IS NOT NULL AND quantity IS NOT NULL AND quantity::int + $4 = 0
THEN form_list - elem_index::int
ELSE jsonb_set(
form_list,
array[elem_index::text, 'quantity'],
to_jsonb(quantity::int + $4),
true
)
WHEN elem_index IS NOT NULL AND quantity IS NOT NULL
THEN jsonb_set(
form_list,
array[elem_index::text, 'quantity'],
to_jsonb(quantity::int + $4),
true
)
ELSE NULL
END
FROM (
SELECT
@ -227,6 +245,10 @@ impl MerchandiseList {
shop_id = $1 AND
elem->>'mod_name' = $2::text AND
elem->>'local_form_id' = $3::text
UNION ALL
SELECT
NULL as elem_index, NULL as quantity
LIMIT 1
) sub
WHERE
shop_id = $1
@ -235,8 +257,26 @@ impl MerchandiseList {
mod_name,
local_form_id,
quantity_delta,
add_item,
)
.fetch_one(db)
.await?)
.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 db_error.code() == Some("23502") && db_error.column_name() == Some("form_list") {
return anyhow!(HttpApiProblem::with_title_and_type_from_status(
StatusCode::NOT_FOUND
)
.set_detail(format!(
"Cannot find merchandise to buy with mod_name: {} and local_form_id: {:#010X}",
mod_name, local_form_id
)));
}
}
anyhow_error
})?)
}
}

View File

@ -10,7 +10,7 @@ pub mod shop;
pub mod transaction;
pub use interior_ref_list::InteriorRefList;
pub use merchandise_list::{MerchandiseList, MerchandiseParams};
pub use merchandise_list::MerchandiseList;
pub use model::{Model, UpdateableModel};
pub use owner::Owner;
pub use shop::Shop;

View File

@ -5,6 +5,14 @@ use url::Url;
use super::ListParams;
// TODO: I stopped using this because I needed to accept a transaction instead of a &PgPool for these methods on certain models.
// It would be nice to find a way to impl this trait for all my models so I don't have to keep redoing the `url` function on
// each. But, maybe I'm trying to use Traits in an OOP way and that's bad, idk.
//
// @NyxCode on discord: "on 0.4, you can use impl Executor<'_, Database = Postgres>. I use it everywhere, and it works for
// &PgPool, &mut PgConnection and &mut Transaction"
//
// I attempted to use `impl Executor<Database = Postgres>` in 0.3.5 but it created a recursive type error :(
#[async_trait]
pub trait Model
where

View File

@ -1,14 +1,13 @@
use anyhow::{Error, Result};
use async_trait::async_trait;
use anyhow::{anyhow, Error, Result};
use chrono::prelude::*;
use ipnetwork::IpNetwork;
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPool;
use sqlx::PgPool;
use tracing::instrument;
use url::Url;
use uuid::Uuid;
use super::ListParams;
use super::{Model, UpdateableModel};
use crate::problem::forbidden_permission;
#[derive(Debug, Serialize, Deserialize, Clone)]
@ -24,18 +23,28 @@ pub struct Owner {
pub updated_at: Option<NaiveDateTime>,
}
#[async_trait]
impl Model for Owner {
fn resource_name() -> &'static str {
impl Owner {
pub fn resource_name() -> &'static str {
"owner"
}
fn pk(&self) -> Option<i32> {
pub fn pk(&self) -> Option<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()
))
}
}
#[instrument(level = "debug", skip(db))]
async fn get(db: &PgPool, id: i32) -> Result<Self> {
pub async fn get(db: &PgPool, id: i32) -> Result<Self> {
sqlx::query_as!(Self, "SELECT * FROM owners WHERE id = $1", id)
.fetch_one(db)
.await
@ -43,7 +52,7 @@ impl Model for Owner {
}
#[instrument(level = "debug", skip(self, db))]
async fn create(self, db: &PgPool) -> Result<Self> {
pub async fn create(self, db: &PgPool) -> Result<Self> {
Ok(sqlx::query_as!(
Self,
"INSERT INTO owners
@ -60,7 +69,7 @@ impl Model for Owner {
}
#[instrument(level = "debug", skip(db))]
async fn delete(db: &PgPool, owner_id: i32, id: i32) -> Result<u64> {
pub async fn delete(db: &PgPool, owner_id: i32, id: i32) -> Result<u64> {
let owner = sqlx::query!("SELECT id FROM owners WHERE id = $1", id)
.fetch_one(db)
.await?;
@ -74,7 +83,7 @@ impl Model for Owner {
}
#[instrument(level = "debug", skip(db))]
async fn list(db: &PgPool, list_params: &ListParams) -> Result<Vec<Self>> {
pub async fn list(db: &PgPool, list_params: &ListParams) -> Result<Vec<Self>> {
let result = if let Some(order_by) = list_params.get_order_by() {
sqlx::query_as!(
Self,
@ -102,12 +111,9 @@ impl Model for Owner {
};
Ok(result)
}
}
#[async_trait]
impl UpdateableModel for Owner {
#[instrument(level = "debug", skip(self, db))]
async fn update(self, db: &PgPool, owner_id: i32, id: i32) -> Result<Self> {
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)
.fetch_one(db)
.await?;

View File

@ -1,12 +1,11 @@
use anyhow::{Error, Result};
use async_trait::async_trait;
use anyhow::{anyhow, Error, Result};
use chrono::prelude::*;
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPool;
use sqlx::PgPool;
use tracing::instrument;
use url::Url;
use super::ListParams;
use super::{Model, UpdateableModel};
use crate::problem::forbidden_permission;
#[derive(Debug, Serialize, Deserialize, Clone)]
@ -24,18 +23,28 @@ pub struct Shop {
pub updated_at: Option<NaiveDateTime>,
}
#[async_trait]
impl Model for Shop {
fn resource_name() -> &'static str {
impl Shop {
pub fn resource_name() -> &'static str {
"shop"
}
fn pk(&self) -> Option<i32> {
pub fn pk(&self) -> Option<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()
))
}
}
#[instrument(level = "debug", skip(db))]
async fn get(db: &PgPool, id: i32) -> Result<Self> {
pub async fn get(db: &PgPool, id: i32) -> Result<Self> {
sqlx::query_as!(Self, "SELECT * FROM shops WHERE id = $1", id)
.fetch_one(db)
.await
@ -43,7 +52,7 @@ impl Model for Shop {
}
#[instrument(level = "debug", skip(self, db))]
async fn create(self, db: &PgPool) -> Result<Self> {
pub async fn create(self, db: &PgPool) -> Result<Self> {
Ok(sqlx::query_as!(
Self,
"INSERT INTO shops
@ -59,7 +68,7 @@ impl Model for Shop {
}
#[instrument(level = "debug", skip(db))]
async fn delete(db: &PgPool, owner_id: i32, id: i32) -> Result<u64> {
pub async fn delete(db: &PgPool, owner_id: i32, id: i32) -> Result<u64> {
let shop = sqlx::query!("SELECT owner_id FROM shops WHERE id = $1", id)
.fetch_one(db)
.await?;
@ -73,7 +82,7 @@ impl Model for Shop {
}
#[instrument(level = "debug", skip(db))]
async fn list(db: &PgPool, list_params: &ListParams) -> Result<Vec<Self>> {
pub async fn list(db: &PgPool, list_params: &ListParams) -> Result<Vec<Self>> {
let result = if let Some(order_by) = list_params.get_order_by() {
sqlx::query_as!(
Self,
@ -101,12 +110,9 @@ impl Model for Shop {
};
Ok(result)
}
}
#[async_trait]
impl UpdateableModel for Shop {
#[instrument(level = "debug", skip(self, db))]
async fn update(self, db: &PgPool, owner_id: i32, id: i32) -> Result<Self> {
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)
.fetch_one(db)
.await?;

View File

@ -1,12 +1,12 @@
use anyhow::{Error, Result};
use async_trait::async_trait;
use anyhow::{anyhow, Error, Result};
use chrono::prelude::*;
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPool;
use sqlx::pool::PoolConnection;
use sqlx::{PgConnection, PgPool};
use tracing::instrument;
use url::Url;
use super::ListParams;
use super::Model;
use crate::problem::forbidden_permission;
#[derive(Debug, Serialize, Deserialize, Clone)]
@ -16,6 +16,10 @@ pub struct Transaction {
pub owner_id: Option<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,
@ -23,18 +27,28 @@ pub struct Transaction {
pub updated_at: Option<NaiveDateTime>,
}
#[async_trait]
impl Model for Transaction {
fn resource_name() -> &'static str {
impl Transaction {
pub fn resource_name() -> &'static str {
"transaction"
}
fn pk(&self) -> Option<i32> {
pub fn pk(&self) -> Option<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()
))
}
}
#[instrument(level = "debug", skip(db))]
async fn get(db: &PgPool, id: i32) -> Result<Self> {
pub async fn get(db: &PgPool, id: i32) -> Result<Self> {
sqlx::query_as!(Self, "SELECT * FROM transactions WHERE id = $1", id)
.fetch_one(db)
.await
@ -42,17 +56,25 @@ impl Model for Transaction {
}
#[instrument(level = "debug", skip(db))]
async fn create(self, db: &PgPool) -> Result<Self> {
pub async fn create(
self,
db: &mut sqlx::Transaction<PoolConnection<PgConnection>>,
) -> Result<Self> {
Ok(sqlx::query_as!(
Self,
"INSERT INTO transactions
(shop_id, owner_id, mod_name, local_form_id, is_sell, quantity, amount, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, now(), now())
(shop_id, owner_id, mod_name, local_form_id, name, form_type, is_food, price,
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,
@ -62,7 +84,7 @@ impl Model for Transaction {
}
#[instrument(level = "debug", skip(db))]
async fn delete(db: &PgPool, owner_id: i32, id: i32) -> Result<u64> {
pub async fn delete(db: &PgPool, owner_id: i32, id: i32) -> Result<u64> {
let transaction = sqlx::query!("SELECT owner_id FROM transactions WHERE id = $1", id)
.fetch_one(db)
.await?;
@ -76,7 +98,7 @@ impl Model for Transaction {
}
#[instrument(level = "debug", skip(db))]
async fn list(db: &PgPool, list_params: &ListParams) -> Result<Vec<Self>> {
pub async fn list(db: &PgPool, list_params: &ListParams) -> Result<Vec<Self>> {
let result = if let Some(order_by) = list_params.get_order_by() {
sqlx::query_as!(
Self,
@ -104,9 +126,7 @@ impl Model for Transaction {
};
Ok(result)
}
}
impl Transaction {
#[instrument(level = "debug", skip(db))]
pub async fn list_by_shop_id(
db: &PgPool,

View File

@ -1,7 +1,11 @@
{
"shop_id": 1,
"mod_name": "Skyrim.esm",
"local_form_id": 1,
"local_form_id": 5,
"name": "New Thing",
"form_type": 41,
"is_food": false,
"price": 100,
"is_sell": false,
"quantity": 1,
"amount": 100