diff --git a/.idea/runConfigurations/Backend.xml b/.idea/runConfigurations/Backend.xml new file mode 100644 index 0000000..adc1c75 --- /dev/null +++ b/.idea/runConfigurations/Backend.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Frontend.xml b/.idea/runConfigurations/Frontend.xml index f783027..bfc0be5 100644 --- a/.idea/runConfigurations/Frontend.xml +++ b/.idea/runConfigurations/Frontend.xml @@ -8,7 +8,7 @@ - + diff --git a/holycow_backend/Cargo.lock b/holycow_backend/Cargo.lock index fb20135..c4d430c 100644 --- a/holycow_backend/Cargo.lock +++ b/holycow_backend/Cargo.lock @@ -26,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.93" @@ -202,7 +217,12 @@ version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", ] [[package]] @@ -318,6 +338,7 @@ checksum = "cbf9649c05e0a9dbd6d0b0b8301db5182b972d0fd02f0a7c6736cf632d7c0fd5" dependencies = [ "bitflags 2.6.0", "byteorder", + "chrono", "diesel_derives", "itoa", "pq-sys", @@ -630,11 +651,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] -name = "holycow_api" +name = "holycow_backend" version = "0.1.0" dependencies = [ "anyhow", "axum", + "chrono", "diesel", "diesel_migrations", "log", @@ -792,6 +814,29 @@ dependencies = [ "tower-service", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -2211,6 +2256,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/holycow_backend/Cargo.toml b/holycow_backend/Cargo.toml index 052fd0e..f71addf 100644 --- a/holycow_backend/Cargo.toml +++ b/holycow_backend/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "holycow_api" +name = "holycow_backend" version = "0.1.0" edition = "2021" [dependencies] anyhow = "1.0.93" axum = { version = "0.7.9", features = ["macros"] } -diesel = { version = "2.2.5", features = ["postgres"] } +diesel = { version = "2.2.5", features = ["chrono", "postgres"] } diesel_migrations = { version = "2.2.0", features = ["postgres"] } log = "0.4.22" micronfig = "0.3.0" @@ -15,3 +15,4 @@ skillratings = "0.27.1" teloxide = { version = "0.13.0", features = ["webhooks-axum"] } tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread", "net"] } serde = { version = "1.0.215", features = ["derive"] } +chrono = "0.4.38" diff --git a/holycow_backend/diesel.toml b/holycow_backend/diesel.toml index 9624cfd..83d15a9 100644 --- a/holycow_backend/diesel.toml +++ b/holycow_backend/diesel.toml @@ -6,4 +6,4 @@ file = "src/schema.rs" custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] [migrations_directory] -dir = "/mnt/work/steffo/holycow_api/migrations" +dir = "./migrations" diff --git a/holycow_backend/migrations/2024-11-27-175800_first/down.sql b/holycow_backend/migrations/2024-11-27-175800_first/down.sql deleted file mode 100644 index df130f5..0000000 --- a/holycow_backend/migrations/2024-11-27-175800_first/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS games; diff --git a/holycow_backend/migrations/2024-11-27-175800_first/up.sql b/holycow_backend/migrations/2024-11-27-175800_first/up.sql deleted file mode 100644 index 601536b..0000000 --- a/holycow_backend/migrations/2024-11-27-175800_first/up.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE games ( - id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - - p1 BPCHAR, - p2 BPCHAR, - - p1result BPCHAR, - - CONSTRAINT not_self CHECK (p1 != p2), - CONSTRAINT acceptable_result CHECK (p1result = 'W' OR p1result = 'T' OR p1result = 'L') -); diff --git a/holycow_backend/migrations/2024-11-28-084240_create players/down.sql b/holycow_backend/migrations/2024-11-28-084240_create players/down.sql new file mode 100644 index 0000000..c9ab14a --- /dev/null +++ b/holycow_backend/migrations/2024-11-28-084240_create players/down.sql @@ -0,0 +1,4 @@ +DROP TABLE IF EXISTS matches; +DROP TYPE IF EXISTS outcome_t; +DROP TABLE IF EXISTS players; +DROP TYPE IF EXISTS wenglin_t; diff --git a/holycow_backend/migrations/2024-11-28-084240_create players/up.sql b/holycow_backend/migrations/2024-11-28-084240_create players/up.sql new file mode 100644 index 0000000..17a930c --- /dev/null +++ b/holycow_backend/migrations/2024-11-28-084240_create players/up.sql @@ -0,0 +1,43 @@ +CREATE TYPE wenglin_t AS ( + rating float8, + uncertainty float8 +); + +CREATE TABLE players ( + id INTEGER GENERATED ALWAYS AS IDENTITY, + + wenglin wenglin_t NOT NULL DEFAULT ROW(25.0, 25.0 / 3), + + telegram_id BIGINT, + + CONSTRAINT telegram_ids_are_unique UNIQUE (telegram_id), + PRIMARY KEY (id) +); + +CREATE TYPE outcome_t AS ENUM ( + 'AWins', + 'BWins', + 'Tie' +); + +CREATE TABLE matches ( + id INTEGER GENERATED ALWAYS AS IDENTITY, + instant TIMESTAMPTZ NOT NULL DEFAULT now(), + name VARCHAR, + + player_a_id BIGINT NOT NULL, + player_a_wenglin_before wenglin_t NOT NULL, + player_a_wenglin_after wenglin_t NOT NULL, + + player_b_id BIGINT NOT NULL, + player_b_wenglin_before wenglin_t NOT NULL, + player_b_wenglin_after wenglin_t NOT NULL, + + outcome outcome_t NOT NULL, + + CONSTRAINT match_unique_name UNIQUE (name), + CONSTRAINT not_same_player CHECK (player_a_id != player_b_id), + FOREIGN KEY (player_a_id) REFERENCES players (id), + FOREIGN KEY (player_b_id) REFERENCES players (id), + PRIMARY KEY (id) +); diff --git a/holycow_backend/src/get_results.sql b/holycow_backend/src/get_results.sql deleted file mode 100644 index 3682072..0000000 --- a/holycow_backend/src/get_results.sql +++ /dev/null @@ -1,7 +0,0 @@ -SELECT - played, - wins -FROM - (SELECT COUNT(*) AS played FROM games WHERE :p = p1 OR :p = p2) AS pt, - (SELECT COUNT(*) AS wins FROM games WHERE (:p = p1 AND p1result = 'W') OR (:p = p2 AND p1result = 'L')) AS wt -; diff --git a/holycow_backend/src/main.rs b/holycow_backend/src/main.rs index d2b95e2..5df52ce 100644 --- a/holycow_backend/src/main.rs +++ b/holycow_backend/src/main.rs @@ -1,13 +1,13 @@ use std::convert::Infallible; use std::process::exit; -use std::sync::Arc; use anyhow::Context; use axum::http::StatusCode; use diesel::{Connection, PgConnection}; use diesel_migrations::MigrationHarness; -use serde::{Deserialize, Serialize}; mod config; +mod schema; +mod types; pub const MIGRATIONS: diesel_migrations::EmbeddedMigrations = diesel_migrations::embed_migrations!(); @@ -55,13 +55,8 @@ async fn main() -> anyhow::Result { exit(1) } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -struct HolyCowResults { - played: u16, - wins: u16, - rating: u16, -} - -fn results_handler() -> Result { +#[axum::debug_handler] +async fn results_handler() -> Result { + todo!() } diff --git a/holycow_backend/src/schema.rs b/holycow_backend/src/schema.rs index 2ef243f..f40ae01 100644 --- a/holycow_backend/src/schema.rs +++ b/holycow_backend/src/schema.rs @@ -1,10 +1,46 @@ // @generated automatically by Diesel CLI. +pub mod sql_types { + #[derive(diesel::query_builder::QueryId, Clone, diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "outcome_t"))] + pub struct OutcomeT; + + #[derive(diesel::query_builder::QueryId, Clone, diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "wenglin_t"))] + pub struct WenglinT; +} + diesel::table! { - games (id) { + use diesel::sql_types::*; + use super::sql_types::WenglinT; + use super::sql_types::OutcomeT; + + matches (id) { id -> Int4, - p1 -> Nullable, - p2 -> Nullable, - p1result -> Nullable, + instant -> Timestamptz, + name -> Nullable, + player_a_id -> Int8, + player_a_wenglin_before -> WenglinT, + player_a_wenglin_after -> WenglinT, + player_b_id -> Int8, + player_b_wenglin_before -> WenglinT, + player_b_wenglin_after -> WenglinT, + outcome -> OutcomeT, } } + +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::WenglinT; + + players (id) { + id -> Int4, + wenglin -> WenglinT, + telegram_id -> Nullable, + } +} + +diesel::allow_tables_to_appear_in_same_query!( + matches, + players, +); diff --git a/holycow_backend/src/types.rs b/holycow_backend/src/types.rs new file mode 100644 index 0000000..3b80b99 --- /dev/null +++ b/holycow_backend/src/types.rs @@ -0,0 +1,137 @@ +use std::io::Write; +use chrono::{DateTime, Utc}; +use diesel::{AsExpression, FromSqlRow, Identifiable, Insertable, Queryable, QueryableByName, Selectable}; +use diesel::backend::Backend; +use diesel::deserialize::FromSql; +use diesel::pg::Pg; +use diesel::serialize::ToSql; +use diesel::sql_types as sql; +use diesel::serialize::Output as DieselOutput; +use crate::schema; + +#[derive(Debug, Clone, FromSqlRow, AsExpression)] +#[diesel(sql_type = sql::BigInt)] +#[diesel(check_for_backend(Pg))] +pub struct TelegramId(pub teloxide::types::ChatId); + +#[derive(Debug, Clone, FromSqlRow, AsExpression)] +#[diesel(sql_type = schema::sql_types::WenglinT)] +#[diesel(check_for_backend(Pg))] +pub struct WengLinRating(pub skillratings::weng_lin::WengLinRating); + +#[derive(Debug, Clone, Queryable, QueryableByName, Identifiable, Selectable)] +#[diesel(table_name = schema::players)] +#[diesel(check_for_backend(Pg))] +pub struct Player { + pub id: i32, + pub wenglin: WengLinRating, + pub telegram_id: Option, +} + +#[derive(Debug, Clone, Insertable)] +#[diesel(table_name = schema::players)] +#[diesel(check_for_backend(Pg))] +pub struct PlayerI { + pub wenglin: WengLinRating, + pub telegram_id: TelegramId, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromSqlRow, AsExpression)] +#[diesel(sql_type = schema::sql_types::OutcomeT)] +pub enum Outcome { + AWins, + BWins, + Tie, +} + +#[derive(Debug, Clone, Queryable, QueryableByName, Identifiable, Selectable)] +#[diesel(table_name = schema::matches)] +#[diesel(check_for_backend(Pg))] +pub struct Match { + pub id: i32, + pub instant: DateTime, + pub name: Option, + pub player_a_id: TelegramId, + pub player_a_wenglin_before: WengLinRating, + pub player_a_wenglin_after: WengLinRating, + pub player_b_id: TelegramId, + pub player_b_wenglin_before: WengLinRating, + pub player_b_wenglin_after: WengLinRating, + pub outcome: Outcome, +} + +#[derive(Debug, Clone, Insertable)] +#[diesel(table_name = schema::matches)] +#[diesel(check_for_backend(Pg))] +pub struct MatchI { + pub instant: DateTime, + pub name: Option, + pub player_a_id: TelegramId, + pub player_a_wenglin_before: WengLinRating, + pub player_a_wenglin_after: WengLinRating, + pub player_b_id: TelegramId, + pub player_b_wenglin_before: WengLinRating, + pub player_b_wenglin_after: WengLinRating, + pub outcome: Outcome, +} + + +impl FromSql for TelegramId { + fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { + let s = >::from_sql(bytes)?; + + Ok(Self(teloxide::types::ChatId(s))) + } +} + +impl FromSql for WengLinRating { + fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { + let rating = >::from_sql(bytes)?; + let uncertainty = >::from_sql(bytes)?; + + let rating = skillratings::weng_lin::WengLinRating::from((rating, uncertainty)); + Ok(Self(rating)) + } +} + +impl FromSql for Outcome { + fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { + let o = match bytes.as_bytes() { + b"AWins" => Self::AWins, + b"BWins" => Self::BWins, + b"Tie" => Self::Tie, + _ => Err(anyhow::Error::msg("unknown enum variant"))? + }; + + Ok(o) + } +} + +impl ToSql for TelegramId { + fn to_sql<'b>(&'b self, out: &mut diesel::serialize::Output<'b, '_, Pg>) -> diesel::serialize::Result { + >::to_sql(&self.0.0, out) + } +} + +impl ToSql for WengLinRating { + fn to_sql<'b>(&'b self, out: &mut DieselOutput<'b, '_, Pg>) -> diesel::serialize::Result { + >::to_sql(&self.0.rating, out)?; + >::to_sql(&self.0.uncertainty, out)?; + + Ok(diesel::serialize::IsNull::No) + } +} + +impl ToSql for Outcome { + fn to_sql<'b>(&'b self, out: &mut DieselOutput<'b, '_, Pg>) -> diesel::serialize::Result { + out.write_all( + match self { + Outcome::AWins => b"AWins", + Outcome::BWins => b"BWins", + Outcome::Tie => b"Tie", + } + )?; + + Ok(diesel::serialize::IsNull::No) + } +} \ No newline at end of file diff --git a/holycow_frontend/package.json b/holycow_frontend/package.json index a7c89aa..a08a9f2 100644 --- a/holycow_frontend/package.json +++ b/holycow_frontend/package.json @@ -1,5 +1,5 @@ { - "name": "holycow", + "name": "holycow_frontend", "version": "0.1.0", "private": true, "scripts": {