move to postgres; implement marking caught fish

This commit is contained in:
insects 2025-02-06 14:25:51 +01:00
parent 3e249e0a0d
commit e564e87c94
7 changed files with 110 additions and 34 deletions

2
Cargo.lock generated
View file

@ -169,6 +169,7 @@ dependencies = [
"axum",
"chrono",
"chrono-humanize",
"dotenvy",
"maud",
"nanoid",
"serde",
@ -922,7 +923,6 @@ version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]

View file

@ -8,6 +8,7 @@ anyhow = "1.0.95"
axum = { version = "0.8.1", features = ["macros"] }
chrono = "0.4.39"
chrono-humanize = "0.2.3"
dotenvy = "0.15.7"
maud = { version = "0.27.0", features = ["axum"] }
nanoid = "0.4.0"
serde = { version = "1.0.217", features = ["derive"] }
@ -16,7 +17,7 @@ serde_path_to_error = "0.1.16"
sqlx = { version = "0.8", features = [
"runtime-tokio",
"tls-rustls-ring-webpki",
"sqlite",
"postgres",
"migrate",
] }
tokio = { version = "1.43.0", features = ["full"] }

View file

@ -1,10 +1,12 @@
-- Add migration script here
CREATE TABLE accounts (account_id VARCHAR(10) PRIMARY KEY);
CREATE TABLE accounts (
account_id VARCHAR(10) PRIMARY KEY NOT NULL UNIQUE
);
CREATE TABLE caught_fish (
id INTEGER PRIMARY KEY,
account_id VARCHAR(10),
fish_id INTEGER,
id SERIAL PRIMARY KEY,
account_id VARCHAR(10) NOT NULL,
fish_id INTEGER NOT NULL,
FOREIGN KEY (account_id) REFERENCES accounts (account_id) ON DELETE CASCADE ON UPDATE NO ACTION
);

View file

@ -1,26 +1,26 @@
use sqlx::{Pool, Sqlite};
use sqlx::{Pool, Postgres};
use crate::AppError;
pub struct Account {
account_id: Option<String>,
pub account_id: String,
}
pub async fn insert_account(id: &str, pool: &Pool<Sqlite>) -> Result<(), AppError> {
sqlx::query!(
"
INSERT INTO accounts (account_id)
VALUES (?)
",
id
)
.execute(pool)
.await?;
pub struct CaughtFish {
pub id: Option<i32>,
pub account_id: String,
pub fish_id: i32,
}
pub async fn insert_account(id: &str, pool: &Pool<Postgres>) -> Result<(), AppError> {
sqlx::query!("INSERT INTO accounts (account_id) VALUES ($1)", id)
.execute(pool)
.await?;
Ok(())
}
pub async fn get_account(id: &str, pool: &Pool<Sqlite>) -> Result<bool, AppError> {
let results = sqlx::query_as!(Account, "SELECT * FROM accounts WHERE account_id = ?", id)
pub async fn get_account(id: &str, pool: &Pool<Postgres>) -> Result<bool, AppError> {
let results = sqlx::query_as!(Account, "SELECT * FROM accounts WHERE account_id = $1", id)
.fetch_all(pool)
.await?;
@ -30,3 +30,33 @@ pub async fn get_account(id: &str, pool: &Pool<Sqlite>) -> Result<bool, AppError
Ok(false)
}
}
pub async fn insert_caught_fish(
acc_id: &str,
fish_id: &i32,
pool: &Pool<Postgres>,
) -> Result<(), AppError> {
sqlx::query!(
"
INSERT INTO caught_fish (account_id, fish_id) VALUES ($1, $2)
",
acc_id,
fish_id
)
.execute(pool)
.await?;
Ok(())
}
pub async fn get_caught_fish(acc_id: &str, pool: &Pool<Postgres>) -> Result<Vec<i32>, AppError> {
let results = sqlx::query_as!(
CaughtFish,
"SELECT * FROM caught_fish WHERE account_id = $1",
acc_id
)
.fetch_all(pool)
.await?;
Ok(results.iter().map(|cf| cf.fish_id).collect())
}

View file

@ -1,16 +1,16 @@
use std::{collections::HashMap, sync::Arc};
use axum::{
extract::{Query, State},
extract::{Path, Query, State},
http::StatusCode,
response::{IntoResponse, Redirect, Response},
routing::get,
routing::{get, post},
Router,
};
use data::Data;
use maud::{html, Markup};
use nanoid::nanoid;
use sqlx::{sqlite::SqlitePoolOptions, Pool, Sqlite};
use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
use tower_http::{services::ServeDir, trace::TraceLayer};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
@ -22,7 +22,7 @@ pub mod templates;
pub struct AppState {
pub data: Data,
pub pool: Pool<Sqlite>,
pub pool: Pool<Postgres>,
}
pub struct AppError(anyhow::Error);
@ -56,8 +56,30 @@ async fn new_account_handler(state: State<Arc<AppState>>) -> Result<Markup, AppE
Ok(templates::new_account(id))
}
async fn main_handler(state: State<Arc<AppState>>) -> Result<Markup, AppError> {
Ok(templates::main_page(state))
async fn main_handler(
state: State<Arc<AppState>>,
Path(acc_id): Path<String>,
) -> Result<Response, AppError> {
let exists = db::get_account(&acc_id, &state.pool).await?;
if !exists {
return Ok(html! { "The provided account ID doesn't eixst." }.into_response());
}
let caught_fish = db::get_caught_fish(&acc_id, &state.pool).await?;
Ok(templates::main_page(state, caught_fish, acc_id).into_response())
}
async fn insert_cf_handler(
state: State<Arc<AppState>>,
Path((acc_id, fish_id)): Path<(String, String)>,
) -> Result<Response, AppError> {
let exists = db::get_account(&acc_id, &state.pool).await?;
if !exists {
return Ok(html! { "The provided account ID doesn't eixst." }.into_response());
}
db::insert_caught_fish(&acc_id, &fish_id.parse::<i32>().unwrap(), &state.pool).await?;
Ok(Redirect::to(&format!("/{}", acc_id)).into_response())
}
async fn to_handler(
@ -81,6 +103,9 @@ async fn to_handler(
#[tokio::main]
async fn main() {
#[cfg(debug_assertions)]
dotenvy::dotenv().unwrap();
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
@ -94,9 +119,9 @@ async fn main() {
.with(tracing_subscriber::fmt::layer())
.init();
let pool = SqlitePoolOptions::new()
let pool = PgPoolOptions::new()
.max_connections(10)
.connect("sqlite://beacon.db")
.connect(&std::env::var("DATABASE_URL").unwrap())
.await
.unwrap();
@ -106,6 +131,7 @@ async fn main() {
.route("/", get(root_handler).post(new_account_handler))
.route("/to", get(to_handler))
.route("/{id}", get(main_handler))
.route("/{acc_id}/catch/{fish_id}", post(insert_cf_handler))
.layer(TraceLayer::new_for_http())
.nest_service("/static", ServeDir::new("static"))
.with_state(Arc::new(AppState {

View file

@ -1,7 +1,7 @@
use std::sync::Arc;
use axum::extract::State;
use maud::{html, Markup, DOCTYPE};
use maud::{html, Markup, PreEscaped, DOCTYPE};
use crate::{clock, data::CombinedFish, AppState};
@ -21,9 +21,16 @@ pub fn layout(content: Markup) -> Markup {
}
}
pub fn main_page(state: State<Arc<AppState>>) -> Markup {
pub fn main_page(state: State<Arc<AppState>>, caught_fish: Vec<i32>, acc_id: String) -> Markup {
let meta = state.data.fish_with_meta();
let mut values: Vec<&CombinedFish> = meta.values().filter(|f| f.entry.big_fish).collect();
let mut values: Vec<&CombinedFish> = meta
.values()
.filter(|f| {
let is_big_fish = f.entry.big_fish;
let is_caught = caught_fish.contains(&(f.entry.id as i32));
is_big_fish && !is_caught
})
.collect();
values.sort_by(|afish, bfish| {
bfish
.is_up
@ -50,9 +57,16 @@ pub fn main_page(state: State<Arc<AppState>>) -> Markup {
@for fish in values {
section.up[fish.is_up].alwaysup[fish.is_always_up] {
.title {
h3 { (fish.meta.name_en) }
.subtitle {
"Patch " (fish.entry.patch)
div {
form action=(format!("/{}/catch/{}", acc_id, fish.entry.id)) method="post" {
button.catch-button type="submit" { (PreEscaped("&check;")) }
}
}
div {
h3 { (fish.meta.name_en) }
.subtitle {
"Patch " (fish.entry.patch)
}
}
}
.when {

View file

@ -13,6 +13,9 @@ section {
.title {
padding: 5px 0;
display: flex;
align-items: center;
gap: 5px;
}
.title h3 {