Files
pawctioneer-bot/src/db/dao/listing_dao.rs
2025-09-05 03:50:09 +00:00

202 lines
7.1 KiB
Rust

//! Listing Data Access Object (DAO)
//!
//! Provides encapsulated CRUD operations for Listing entities
use anyhow::Result;
use chrono::Utc;
use itertools::Itertools;
use sqlx::{sqlite::SqliteRow, FromRow, Row, SqlitePool};
use std::fmt::Debug;
use crate::db::{
bind_fields::BindFields,
listing::{
BasicAuctionFields, BlindAuctionFields, FixedPriceListingFields, Listing, ListingBase,
ListingFields, MultiSlotAuctionFields, NewListing, PersistedListing,
PersistedListingFields,
},
ListingDbId, ListingType, UserDbId,
};
/// Data Access Object for Listing operations
#[derive(Clone)]
pub struct ListingDAO(SqlitePool);
impl ListingDAO {
pub fn new(pool: SqlitePool) -> Self {
Self(pool)
}
/// Insert a new listing into the database
pub async fn insert_listing(&self, listing: NewListing) -> Result<PersistedListing> {
let now = Utc::now();
let binds = binds_for_listing(&listing)
.push("seller_id", &listing.base.seller_id)
.push("starts_at", &listing.base.starts_at)
.push("ends_at", &listing.base.ends_at)
.push("created_at", &now)
.push("updated_at", &now);
let query_str = format!(
r#"
INSERT INTO listings ({}) VALUES ({})
RETURNING *
"#,
binds.bind_names().join(", "),
binds.bind_placeholders().join(", "),
);
let row = binds
.bind_to_query(sqlx::query(&query_str))
.fetch_one(&self.0)
.await?;
Ok(FromRow::from_row(&row)?)
}
pub async fn update_listing(&self, listing: PersistedListing) -> Result<PersistedListing> {
let now = Utc::now();
let binds = binds_for_listing(&listing).push("updated_at", &now);
let query_str = format!(
r#"
UPDATE listings
SET {}
WHERE id = ?
AND seller_id = ?
RETURNING *
"#,
binds
.bind_names()
.map(|name| format!("{name} = ?"))
.join(", "),
);
let row = binds
.bind_to_query(sqlx::query(&query_str))
.bind(listing.persisted.id)
.bind(listing.base.seller_id)
.fetch_one(&self.0)
.await?;
Ok(FromRow::from_row(&row)?)
}
/// Find a listing by its ID
pub async fn find_by_id(&self, listing_id: ListingDbId) -> Result<Option<PersistedListing>> {
let result = sqlx::query_as("SELECT * FROM listings WHERE id = ?")
.bind(listing_id)
.fetch_optional(&self.0)
.await?;
Ok(result)
}
/// Find all listings by a seller
pub async fn find_by_seller(&self, seller_id: UserDbId) -> Result<Vec<PersistedListing>> {
let rows =
sqlx::query_as("SELECT * FROM listings WHERE seller_id = ? ORDER BY created_at DESC")
.bind(seller_id)
.fetch_all(&self.0)
.await?;
Ok(rows)
}
/// Delete a listing
pub async fn delete_listing(&self, listing_id: ListingDbId) -> Result<()> {
sqlx::query("DELETE FROM listings WHERE id = ?")
.bind(listing_id)
.execute(&self.0)
.await?;
Ok(())
}
}
fn binds_for_listing<P: Debug + Clone>(listing: &Listing<P>) -> BindFields {
BindFields::default()
.extend(binds_for_base(&listing.base))
.extend(binds_for_fields(&listing.fields))
}
fn binds_for_base(base: &ListingBase) -> BindFields {
BindFields::default()
.push("title", &base.title)
.push("description", &base.description)
.push("currency_type", &base.currency_type)
}
fn binds_for_fields(fields: &ListingFields) -> BindFields {
match fields {
ListingFields::BasicAuction(fields) => BindFields::default()
.push("listing_type", &ListingType::BasicAuction)
.push("starting_bid", &fields.starting_bid)
.push("buy_now_price", &fields.buy_now_price)
.push("min_increment", &fields.min_increment)
.push("anti_snipe_minutes", &fields.anti_snipe_minutes),
ListingFields::MultiSlotAuction(fields) => BindFields::default()
.push("listing_type", &ListingType::MultiSlotAuction)
.push("starting_bid", &fields.starting_bid)
.push("buy_now_price", &fields.buy_now_price)
.push("min_increment", &fields.min_increment)
.push("slots_available", &fields.slots_available)
.push("anti_snipe_minutes", &fields.anti_snipe_minutes),
ListingFields::FixedPriceListing(fields) => BindFields::default()
.push("listing_type", &ListingType::FixedPriceListing)
.push("buy_now_price", &fields.buy_now_price)
.push("slots_available", &fields.slots_available),
ListingFields::BlindAuction(fields) => BindFields::default()
.push("listing_type", &ListingType::BlindAuction)
.push("starting_bid", &fields.starting_bid),
}
}
impl FromRow<'_, SqliteRow> for PersistedListing {
fn from_row(row: &'_ SqliteRow) -> std::result::Result<Self, sqlx::Error> {
let listing_type = row.get("listing_type");
let persisted = PersistedListingFields {
id: row.get("id"),
created_at: row.get("created_at"),
updated_at: row.get("updated_at"),
};
let base = ListingBase {
seller_id: row.get("seller_id"),
title: row.get("title"),
description: row.get("description"),
currency_type: row.get("currency_type"),
starts_at: row.get("starts_at"),
ends_at: row.get("ends_at"),
};
let fields = match listing_type {
ListingType::BasicAuction => ListingFields::BasicAuction(BasicAuctionFields {
starting_bid: row.get("starting_bid"),
buy_now_price: row.get("buy_now_price"),
min_increment: row.get("min_increment"),
anti_snipe_minutes: row.get("anti_snipe_minutes"),
}),
ListingType::MultiSlotAuction => {
ListingFields::MultiSlotAuction(MultiSlotAuctionFields {
starting_bid: row.get("starting_bid"),
buy_now_price: row.get("buy_now_price"),
min_increment: row.get("min_increment"),
slots_available: row.get("slots_available"),
anti_snipe_minutes: row.get("anti_snipe_minutes"),
})
}
ListingType::FixedPriceListing => {
ListingFields::FixedPriceListing(FixedPriceListingFields {
buy_now_price: row.get("buy_now_price"),
slots_available: row.get("slots_available"),
})
}
ListingType::BlindAuction => ListingFields::BlindAuction(BlindAuctionFields {
starting_bid: row.get("starting_bid"),
}),
};
Ok(PersistedListing {
persisted,
base,
fields,
})
}
}