From 40331451ca674a8eb26e816c05b9f2befbd9d068 Mon Sep 17 00:00:00 2001 From: Alex Selimov Date: Thu, 19 Mar 2026 10:05:38 -0400 Subject: [PATCH] Add repository commands for handling votes --- .gitignore | 1 + Cargo.lock | 419 ++++++++++++++++++++++++++++- Cargo.toml | 11 +- db/migrations/001_create_votes.sql | 8 + docker-compose.yml | 16 ++ src/lib.rs | 1 + src/test_helpers.rs | 16 ++ src/votes/model.rs | 19 +- src/votes/repository.rs | 160 ++++++++++- 9 files changed, 647 insertions(+), 4 deletions(-) create mode 100644 db/migrations/001_create_votes.sql create mode 100644 docker-compose.yml create mode 100644 src/test_helpers.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..fedaa2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.env diff --git a/Cargo.lock b/Cargo.lock index 87ecf26..8990580 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,21 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[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.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "atoi" version = "2.0.0" @@ -120,6 +135,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + [[package]] name = "byteorder" version = "1.5.0" @@ -132,12 +153,35 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cc" +version = "1.2.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -153,6 +197,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -289,6 +339,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "flume" version = "0.11.1" @@ -407,6 +463,19 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -553,6 +622,30 @@ dependencies = [ "tower-service", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "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 = "2.1.1" @@ -634,6 +727,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" version = "1.1.0" @@ -663,6 +762,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -671,6 +772,16 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -680,6 +791,12 @@ dependencies = [ "spin", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.183" @@ -951,6 +1068,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -969,6 +1096,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -996,7 +1129,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.17", ] [[package]] @@ -1054,6 +1187,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" version = "1.0.23" @@ -1066,6 +1205,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -1163,6 +1308,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -1248,6 +1399,7 @@ checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ "base64", "bytes", + "chrono", "crc", "crossbeam-queue", "either", @@ -1272,6 +1424,7 @@ dependencies = [ "tokio-stream", "tracing", "url", + "uuid", ] [[package]] @@ -1323,6 +1476,7 @@ dependencies = [ "bitflags", "byteorder", "bytes", + "chrono", "crc", "digest", "dotenvy", @@ -1351,6 +1505,7 @@ dependencies = [ "stringprep", "thiserror", "tracing", + "uuid", "whoami", ] @@ -1364,6 +1519,7 @@ dependencies = [ "base64", "bitflags", "byteorder", + "chrono", "crc", "dotenvy", "etcetera", @@ -1388,6 +1544,7 @@ dependencies = [ "stringprep", "thiserror", "tracing", + "uuid", "whoami", ] @@ -1398,6 +1555,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", + "chrono", "flume", "futures-channel", "futures-core", @@ -1413,6 +1571,7 @@ dependencies = [ "thiserror", "tracing", "url", + "uuid", ] [[package]] @@ -1698,11 +1857,19 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "uprs" version = "0.1.0" dependencies = [ + "anyhow", "axum", + "chrono", "serde", "sqlx", "tokio", @@ -1710,6 +1877,7 @@ dependencies = [ "tower-http", "tracing", "tracing-subscriber", + "uuid", ] [[package]] @@ -1730,6 +1898,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -1754,12 +1933,109 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasite" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "whoami" version = "1.6.1" @@ -1770,12 +2046,65 @@ dependencies = [ "wasite", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -1851,6 +2180,94 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "writeable" version = "0.6.2" diff --git a/Cargo.toml b/Cargo.toml index dfaaeb0..1cdd092 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,11 +4,20 @@ version = "0.1.0" edition = "2024" [dependencies] +anyhow = "1" axum = "0.8" +chrono = "0.4.44" serde = { version = "1", features = ["derive"] } -sqlx = { version = "0.8.6", features = ["runtime-tokio", "postgres", "macros"] } +sqlx = { version = "0.8.6", features = [ + "runtime-tokio", + "postgres", + "macros", + "uuid", + "chrono", +] } tokio = { version = "1", features = ["full"] } tower = "0.5" tower-http = { version = "0.6", features = ["trace"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +uuid = { version = "1.22.0", features = ["v4"] } diff --git a/db/migrations/001_create_votes.sql b/db/migrations/001_create_votes.sql new file mode 100644 index 0000000..fcc08ea --- /dev/null +++ b/db/migrations/001_create_votes.sql @@ -0,0 +1,8 @@ +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +CREATE TABLE IF NOT EXISTS votes ( + slug text not null, + voter_id uuid not null, + created_at timestamptz NOT NULL DEFAULT timezone('utc'::text, now()), + primary key (slug, voter_id) +); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..86f7a6f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +services: + db: + image: postgres:16 + container_name: uprs_db + restart: always + environment: + POSTGRES_USER: uprs + POSTGRES_PASSWORD: password123 + POSTGRES_DB: uprs + volumes: + - pgdata:/var/lib/postgresql/data + - ./db/migrations:/docker-entrypoint-initdb.d # run initial schema + ports: + - "5432:5432" +volumes: + pgdata: diff --git a/src/lib.rs b/src/lib.rs index 7f9f673..de4b4f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub mod env; pub mod routes; pub mod state; +pub mod test_helpers; pub mod votes; use axum::Router; diff --git a/src/test_helpers.rs b/src/test_helpers.rs new file mode 100644 index 0000000..1ea5d68 --- /dev/null +++ b/src/test_helpers.rs @@ -0,0 +1,16 @@ +#[cfg(test)] +pub mod db { + use sqlx::PgPool; + + const DEFAULT_CONNECTION_STRING: &str = + "postgres://uprs:password123@localhost:5432/uprs"; + + pub async fn test_pool() -> PgPool { + let conn = std::env::var("POSTGRES_CONNECTION_STRING") + .unwrap_or_else(|_| DEFAULT_CONNECTION_STRING.to_string()); + + PgPool::connect(&conn) + .await + .expect("failed to connect to test database") + } +} diff --git a/src/votes/model.rs b/src/votes/model.rs index 52d2039..df5e2e0 100644 --- a/src/votes/model.rs +++ b/src/votes/model.rs @@ -1 +1,18 @@ -pub struct Vote {} +use chrono::{DateTime, Utc}; +use sqlx::prelude::FromRow; +use uuid::Uuid; + +/// Struct representing a single vote (row) in the votes table +#[derive(Debug, FromRow)] +pub struct Vote { + pub slug: String, + pub voter_id: Uuid, + pub created_at: DateTime, +} + +/// Struct representing the best slug posts +#[derive(Debug, FromRow)] +pub struct BestSlugs { + pub slug: String, + pub vote_count: i64, +} diff --git a/src/votes/repository.rs b/src/votes/repository.rs index 42e969f..e890721 100644 --- a/src/votes/repository.rs +++ b/src/votes/repository.rs @@ -1,3 +1,161 @@ -use sqlx::PgPool; +use anyhow::Result; +use sqlx::{PgPool, query, query_as, query_scalar}; +use uuid::Uuid; + +use crate::votes::model::BestSlugs; use super::model::Vote; + +pub async fn insert_new_vote(vote: &Vote, db: &PgPool) -> Result<()> { + query("insert into votes (slug, voter_id) values ($1, $2)") + .bind(vote.slug.clone()) + .bind(vote.voter_id) + .execute(db) + .await?; + Ok(()) +} + +pub async fn get_vote_count_for_slug(slug: &str, db: &PgPool) -> Result { + let count: i64 = query_scalar("select count(*) from votes where slug=$1") + .bind(slug) + .fetch_one(db) + .await?; + Ok(count) +} + +pub async fn get_top_n_slugs(n: i64, db: &PgPool) -> Result> { + if n > 0 { + let top_slugs = query_as::<_, BestSlugs>( + r#"select slug, count(*) AS vote_count + from votes + group by slug + order by vote_count desc + limit $1 + "#, + ) + .bind(n) + .fetch_all(db) + .await?; + Ok(top_slugs) + } else { + Ok(vec![]) + } +} + +pub async fn delete_vote(slug: &str, voter_id: &Uuid, db: &PgPool) -> Result<()> { + query("delete from votes where slug=$1 and voter_id=$2") + .bind(slug) + .bind(voter_id) + .execute(db) + .await?; + Ok(()) +} + +#[cfg(test)] +mod postgres_tests { + use sqlx::PgPool; + use uuid::Uuid; + + use crate::{ + test_helpers::db::test_pool, + votes::{ + model::Vote, + repository::{delete_vote, get_top_n_slugs, get_vote_count_for_slug, insert_new_vote}, + }, + }; + + async fn cleanup(db: &PgPool) { + for vote in test_votes() { + delete_vote(&vote.slug, &vote.voter_id, db).await.unwrap() + } + } + + fn test_votes() -> [Vote; 9] { + [ + Vote { + slug: "blog_post1".into(), + voter_id: Uuid::from_u128(0x1), + created_at: chrono::Utc::now(), + }, + Vote { + slug: "blog_post1".into(), + voter_id: Uuid::from_u128(0x2), + created_at: chrono::Utc::now(), + }, + Vote { + slug: "blog_post2".into(), + voter_id: Uuid::from_u128(0x3), + created_at: chrono::Utc::now(), + }, + Vote { + slug: "blog_post2".into(), + voter_id: Uuid::from_u128(0x4), + created_at: chrono::Utc::now(), + }, + Vote { + slug: "blog_post3".into(), + voter_id: Uuid::from_u128(0x5), + created_at: chrono::Utc::now(), + }, + Vote { + slug: "blog_post3".into(), + voter_id: Uuid::from_u128(0x6), + created_at: chrono::Utc::now(), + }, + Vote { + slug: "blog_post1".into(), + voter_id: Uuid::from_u128(0x7), + created_at: chrono::Utc::now(), + }, + Vote { + slug: "blog_post1".into(), + voter_id: Uuid::from_u128(0x8), + created_at: chrono::Utc::now(), + }, + Vote { + slug: "blog_post3".into(), + voter_id: Uuid::from_u128(0x9), + created_at: chrono::Utc::now(), + }, + ] + } + + #[tokio::test] + #[ignore] + pub async fn postgres_tests() { + let db = test_pool().await; + cleanup(&db).await; + let votes = test_votes(); + + for vote in votes.iter() { + insert_new_vote(vote, &db) + .await + .expect("Insertions to db failed"); + } + + assert_eq!(get_vote_count_for_slug("blog_post1", &db).await.unwrap(), 4); + assert_eq!(get_vote_count_for_slug("blog_post2", &db).await.unwrap(), 2); + assert_eq!(get_vote_count_for_slug("blog_post3", &db).await.unwrap(), 3); + + let top_2 = get_top_n_slugs(2, &db).await.unwrap(); + assert_eq!(top_2[0].slug, "blog_post1"); + assert_eq!(top_2[1].slug, "blog_post3"); + + delete_vote(&votes[4].slug, &votes[4].voter_id, &db) + .await + .unwrap(); + delete_vote(&votes[5].slug, &votes[5].voter_id, &db) + .await + .unwrap(); + + assert_eq!(get_vote_count_for_slug("blog_post1", &db).await.unwrap(), 4); + assert_eq!(get_vote_count_for_slug("blog_post2", &db).await.unwrap(), 2); + assert_eq!(get_vote_count_for_slug("blog_post3", &db).await.unwrap(), 1); + + let top_2 = get_top_n_slugs(2, &db).await.unwrap(); + assert_eq!(top_2[0].slug, "blog_post1"); + assert_eq!(top_2[1].slug, "blog_post2"); + + cleanup(&db).await; + } +}