From e6224ddf1714c3f70124822c843beb528abb7fd6 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Sun, 12 Jan 2025 07:34:34 +0530 Subject: [PATCH 01/38] remove soon-to-be outdated information in readme --- README.md | 36 +----------------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/README.md b/README.md index 1cfb800..99bbc49 100644 --- a/README.md +++ b/README.md @@ -6,40 +6,6 @@ ### Overview **Root** is a backend for managing all club related info; most other projects will be getting or publishing their data to root. -### Setup Instructions - -#### Prerequisites -1. Ensure you have Rust installed. Use `rustup` for easy installation. - ```bash - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - source $HOME/.cargo/env - ``` -2. Install the Shuttle CLI. - ```bash - cargo install shuttle-cli - ``` - -3. Install Docker. Check [this](https://docs.docker.com/desktop/setup/install/linux/) out for instructions. - -#### Clone the Repository -```bash -git clone https://github.com/amfoss/root.git -cd root -``` - -#### Set up Secrets -Create a `Secrets.toml` file in the root directory with the secret key -``` -ROOT_SECRET='secret_key' -``` - -#### Run Locally -```bash -cargo shuttle run -``` - ---- - ### Documentation Explore the [Documentation](/docs/docs.md) for detailed information and usage guidelines. @@ -55,4 +21,4 @@ Explore the [Documentation](/docs/docs.md) for detailed information and usage gu 5. Submit a pull request, referencing the relevant issue number. ### License -This project is licensed under GNU General Public License V3. You are welcome to adapt it, make it yours. Just make sure that you credit us too. \ No newline at end of file +This project is licensed under GNU General Public License V3. You are welcome to adapt it, make it yours. Just make sure that you credit us too. From 4f4ee27d56ffeee0cd451e944841214cdfb48d74 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Sun, 12 Jan 2025 07:43:07 +0530 Subject: [PATCH 02/38] remove shuttle dependency --- Cargo.lock | 1988 ++++++++++++++++++--------------------------------- Cargo.toml | 3 - src/main.rs | 19 +- 3 files changed, 705 insertions(+), 1305 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0eea7a6..e8efa54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,18 +14,18 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "ahash" @@ -62,9 +62,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-tzdata" @@ -81,12 +81,6 @@ dependencies = [ "libc", ] -[[package]] -name = "anyhow" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" - [[package]] name = "ascii_utils" version = "0.9.3" @@ -95,9 +89,9 @@ checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" [[package]] name = "async-graphql" -version = "7.0.6" +version = "7.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf338d20ba5bab309f55ce8df95d65ee19446f7737f06f4a64593ab2c6b546ad" +checksum = "59fd6bd734afb8b6e4d0f84a3e77305ce0a7ccc60d70f6001cb5e1c3f38d8ff1" dependencies = [ "async-graphql-derive", "async-graphql-parser", @@ -109,14 +103,14 @@ dependencies = [ "chrono", "fast_chemail", "fnv", + "futures-timer", "futures-util", "handlebars", - "http 1.1.0", - "indexmap 2.2.6", + "http 1.2.0", + "indexmap", "mime", "multer", "num-traits", - "once_cell", "pin-project-lite", "regex", "serde", @@ -124,18 +118,18 @@ dependencies = [ "serde_urlencoded", "static_assertions_next", "tempfile", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "async-graphql-axum" -version = "7.0.6" +version = "7.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28f874ad4bc10519f3fa500e36814452033a5ce9ea681ab0a2e0d3b1f18bae44" +checksum = "ec8c1bb47161c37286e40e2fa58055e97b2a2b6cf1022a6686967e10636fa5d7" dependencies = [ "async-graphql", "async-trait", - "axum 0.7.5", + "axum", "bytes", "futures-util", "serde_json", @@ -147,9 +141,9 @@ dependencies = [ [[package]] name = "async-graphql-derive" -version = "7.0.6" +version = "7.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc51fd6b7102acda72bc94e8ae1543844d5688ff394a6cf7c21f2a07fe2d64e4" +checksum = "ac38b4dd452d529d6c0248b51df23603f0a875770352e26ae8c346ce6c149b3e" dependencies = [ "Inflector", "async-graphql-parser", @@ -157,16 +151,16 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "strum 0.26.3", - "syn 2.0.68", - "thiserror", + "strum", + "syn 2.0.96", + "thiserror 1.0.69", ] [[package]] name = "async-graphql-parser" -version = "7.0.6" +version = "7.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75361eefd64e39f89bead4cb45fddbaf60ddb0e7b15fb7c852b6088bcd63071f" +checksum = "42d271ddda2f55b13970928abbcbc3423cfc18187c60e8769b48f21a93b7adaa" dependencies = [ "async-graphql-value", "pest", @@ -176,21 +170,21 @@ dependencies = [ [[package]] name = "async-graphql-value" -version = "7.0.6" +version = "7.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1f665d2d52b41c4ed1f01c43f3ef27a2fe0af2452ed5c8bc7ac9b1a8719afaa" +checksum = "aefe909173a037eaf3281b046dc22580b59a38b765d7b8d5116f2ffef098048d" dependencies = [ "bytes", - "indexmap 2.2.6", + "indexmap", "serde", "serde_json", ] [[package]] name = "async-stream" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", @@ -199,24 +193,24 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.96", ] [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.96", ] [[package]] @@ -230,53 +224,25 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" - -[[package]] -name = "axum" -version = "0.6.20" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" -dependencies = [ - "async-trait", - "axum-core 0.3.4", - "bitflags 1.3.2", - "bytes", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.29", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "sync_wrapper 0.1.2", - "tower 0.4.13", - "tower-layer", - "tower-service", -] +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" -version = "0.7.5" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", - "axum-core 0.4.3", - "base64 0.21.7", + "axum-core", + "base64 0.22.1", "bytes", "futures-util", - "http 1.1.0", - "http-body 1.0.0", + "http 1.2.0", + "http-body 1.0.1", "http-body-util", - "hyper 1.3.1", + "hyper 1.5.2", "hyper-util", "itoa", "matchit", @@ -290,10 +256,10 @@ dependencies = [ "serde_path_to_error", "serde_urlencoded", "sha1", - "sync_wrapper 1.0.1", + "sync_wrapper 1.0.2", "tokio", "tokio-tungstenite", - "tower 0.4.13", + "tower", "tower-layer", "tower-service", "tracing", @@ -301,37 +267,20 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "mime", - "rustversion", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" dependencies = [ "async-trait", "bytes", "futures-util", - "http 1.1.0", - "http-body 1.0.0", + "http 1.2.0", + "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", "rustversion", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.2", "tower-layer", "tower-service", "tracing", @@ -339,17 +288,17 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -384,9 +333,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" dependencies = [ "serde", ] @@ -414,18 +363,21 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" dependencies = [ "serde", ] [[package]] name = "cc" -version = "1.0.100" +version = "1.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c891175c3fb232128f48de6590095e59198bbeb8620c310be349bfc3afd12c7b" +checksum = "ad0cf6e91fde44c773c6ee7ec6bba798504641a8bc2eb7e37a04ffbf4dfaa55a" +dependencies = [ + "shlex", +] [[package]] name = "cfg-if" @@ -435,17 +387,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", - "serde", "wasm-bindgen", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -469,28 +420,6 @@ dependencies = [ "phf_codegen", ] -[[package]] -name = "colored" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" -dependencies = [ - "lazy_static", - "windows-sys 0.48.0", -] - -[[package]] -name = "comfy-table" -version = "6.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e959d788268e3bf9d35ace83e81b124190378e4c91c9067524675e33394b8ba" -dependencies = [ - "crossterm 0.26.1", - "strum 0.24.1", - "strum_macros 0.24.3", - "unicode-width", -] - [[package]] name = "config" version = "0.13.4" @@ -528,15 +457,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -556,70 +485,20 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" -[[package]] -name = "crossbeam-channel" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-queue" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" - -[[package]] -name = "crossterm" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" -dependencies = [ - "bitflags 1.3.2", - "crossterm_winapi", - "libc", - "mio", - "parking_lot", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" -dependencies = [ - "bitflags 2.5.0", - "crossterm_winapi", - "libc", - "mio", - "parking_lot", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" -dependencies = [ - "winapi", -] +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" @@ -633,9 +512,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core", "darling_macro", @@ -643,27 +522,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.68", + "syn 2.0.96", ] [[package]] name = "darling_macro" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.68", + "syn 2.0.96", ] [[package]] @@ -683,53 +562,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "diesel" -version = "2.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff236accb9a5069572099f0b350a92e9560e8e63a9b8d546162f4a5e03026bb2" -dependencies = [ - "bitflags 2.5.0", - "byteorder", - "diesel_derives", - "itoa", -] - -[[package]] -name = "diesel-async" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acada1517534c92d3f382217b485db8a8638f111b0e3f2a2a8e26165050f77be" -dependencies = [ - "async-trait", - "diesel", - "futures-util", - "scoped-futures", - "tokio", - "tokio-postgres", -] - -[[package]] -name = "diesel_derives" -version = "2.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14701062d6bed917b5c7103bdffaee1e4609279e240488ad24e7bd979ca6866c" -dependencies = [ - "diesel_table_macro_syntax", - "proc-macro2", - "quote", - "syn 2.0.68", -] - -[[package]] -name = "diesel_table_macro_syntax" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" -dependencies = [ - "syn 2.0.68", -] - [[package]] name = "digest" version = "0.10.7" @@ -742,6 +574,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "dlv-list" version = "0.3.0" @@ -756,18 +599,18 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "either" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" dependencies = [ "serde", ] [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -780,12 +623,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -805,12 +648,6 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - [[package]] name = "fast_chemail" version = "0.9.6" @@ -822,15 +659,15 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "flume" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", @@ -869,9 +706,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -879,15 +716,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -907,38 +744,44 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.96", ] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-io", @@ -974,15 +817,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" - -[[package]] -name = "glob" -version = "0.3.1" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "h2" @@ -996,7 +833,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.2.6", + "indexmap", "slab", "tokio", "tokio-util", @@ -1014,7 +851,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1036,6 +873,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + [[package]] name = "hashlink" version = "0.8.4" @@ -1060,12 +903,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "hex" version = "0.4.3" @@ -1092,11 +929,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1112,9 +949,9 @@ dependencies = [ [[package]] name = "http" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" dependencies = [ "bytes", "fnv", @@ -1134,12 +971,12 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.1.0", + "http 1.2.0", ] [[package]] @@ -1150,16 +987,16 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http 1.1.0", - "http-body 1.0.0", + "http 1.2.0", + "http-body 1.0.1", "pin-project-lite", ] [[package]] name = "httparse" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" @@ -1169,9 +1006,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.29" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -1193,15 +1030,15 @@ dependencies = [ [[package]] name = "hyper" -version = "1.3.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.1.0", - "http-body 1.0.0", + "http 1.2.0", + "http-body 1.0.1", "httparse", "httpdate", "itoa", @@ -1210,18 +1047,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "hyper-timeout" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" -dependencies = [ - "hyper 0.14.29", - "pin-project-lite", - "tokio", - "tokio-io-timeout", -] - [[package]] name = "hyper-tls" version = "0.5.0" @@ -1229,7 +1054,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper 0.14.29", + "hyper 0.14.32", "native-tls", "tokio", "tokio-native-tls", @@ -1237,24 +1062,25 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.5" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", "futures-util", - "http 1.1.0", - "http-body 1.0.0", - "hyper 1.3.1", + "http 1.2.0", + "http-body 1.0.1", + "hyper 1.5.2", "pin-project-lite", "tokio", + "tower-service", ] [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1273,6 +1099,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1281,32 +1225,33 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", ] [[package]] -name = "indexmap" -version = "1.9.3" +name = "idna_adapter" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ - "autocfg", - "hashbrown 0.12.3", + "icu_normalizer", + "icu_properties", ] [[package]] name = "indexmap" -version = "2.2.6" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.15.2", "serde", ] @@ -1316,27 +1261,19 @@ version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -1362,15 +1299,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.155" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libsqlite3-sys" @@ -1391,9 +1328,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "lock_api" @@ -1407,18 +1350,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" - -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "matchit" @@ -1456,23 +1390,22 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ - "adler", + "adler2", ] [[package]] name = "mio" -version = "0.8.11" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "log", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1484,7 +1417,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http 1.1.0", + "http 1.2.0", "httparse", "memchr", "mime", @@ -1519,16 +1452,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1576,30 +1499,20 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "object" -version = "0.36.0" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "openssl" @@ -1607,7 +1520,7 @@ version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.7.0", "cfg-if", "foreign-types", "libc", @@ -1624,7 +1537,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.96", ] [[package]] @@ -1645,63 +1558,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "opentelemetry" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e32339a5dc40459130b3bd269e9892439f55b33e772d2a9d402a789baaf4e8a" -dependencies = [ - "futures-core", - "futures-sink", - "indexmap 2.2.6", - "js-sys", - "once_cell", - "pin-project-lite", - "thiserror", - "urlencoding", -] - -[[package]] -name = "opentelemetry-http" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f51189ce8be654f9b5f7e70e49967ed894e84a06fc35c6c042e64ac1fc5399e" -dependencies = [ - "async-trait", - "bytes", - "http 0.2.12", - "opentelemetry", -] - -[[package]] -name = "opentelemetry_sdk" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f16aec8a98a457a52664d69e0091bac3a0abd18ead9b641cb00202ba4e0efe4" -dependencies = [ - "async-trait", - "crossbeam-channel", - "futures-channel", - "futures-executor", - "futures-util", - "glob", - "once_cell", - "opentelemetry", - "ordered-float", - "percent-encoding", - "rand", - "thiserror", -] - -[[package]] -name = "ordered-float" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e" -dependencies = [ - "num-traits", -] - [[package]] name = "ordered-multimap" version = "0.4.3" @@ -1712,12 +1568,6 @@ dependencies = [ "hashbrown 0.12.3", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "parking_lot" version = "0.12.3" @@ -1736,9 +1586,9 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.2", + "redox_syscall", "smallvec", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -1779,20 +1629,20 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.10" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" +checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", - "thiserror", + "thiserror 2.0.11", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.7.10" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" +checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" dependencies = [ "pest", "pest_generator", @@ -1800,22 +1650,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.10" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" +checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.96", ] [[package]] name = "pest_meta" -version = "2.7.10" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" +checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" dependencies = [ "once_cell", "pest", @@ -1824,18 +1674,18 @@ dependencies = [ [[package]] name = "phf" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_shared", ] [[package]] name = "phf_codegen" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ "phf_generator", "phf_shared", @@ -1843,9 +1693,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", "rand", @@ -1853,38 +1703,18 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher", ] -[[package]] -name = "pin-project" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.68", -] - [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -1915,124 +1745,42 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" - -[[package]] -name = "postgres-protocol" -version = "0.6.6" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b6c5ef183cd3ab4ba005f1ca64c21e8bd97ce4699cfea9e8d9a2c4958ca520" -dependencies = [ - "base64 0.21.7", - "byteorder", - "bytes", - "fallible-iterator", - "hmac", - "md-5", - "memchr", - "rand", - "sha2", - "stringprep", -] +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] -name = "postgres-types" -version = "0.2.6" +name = "ppv-lite86" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d2234cdee9408b523530a9b6d2d6b373d1db34f6a8e51dc03ded1828d7fb67c" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "bytes", - "fallible-iterator", - "postgres-protocol", + "zerocopy", ] -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - [[package]] name = "proc-macro-crate" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ "toml_edit", ] -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] -[[package]] -name = "prost" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-derive" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" -dependencies = [ - "anyhow", - "itertools", - "proc-macro2", - "quote", - "syn 2.0.68", -] - -[[package]] -name = "prost-types" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" -dependencies = [ - "prost", -] - [[package]] name = "quote" -version = "1.0.36" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -2069,65 +1817,41 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.2" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.7.0", ] [[package]] name = "regex" -version = "1.10.5" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" @@ -2143,7 +1867,7 @@ dependencies = [ "h2", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.32", "hyper-tls", "ipnet", "js-sys", @@ -2169,21 +1893,6 @@ dependencies = [ "winreg", ] -[[package]] -name = "ring" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" -dependencies = [ - "cc", - "cfg-if", - "getrandom", - "libc", - "spin", - "untrusted", - "windows-sys 0.52.0", -] - [[package]] name = "ron" version = "0.7.1" @@ -2201,7 +1910,7 @@ version = "0.1.0" dependencies = [ "async-graphql", "async-graphql-axum", - "axum 0.7.5", + "axum", "chrono", "chrono-tz", "config", @@ -2211,20 +1920,17 @@ dependencies = [ "serde", "serde_json", "sha2", - "shuttle-axum", - "shuttle-runtime", - "shuttle-shared-db", "sqlx", "tokio", - "tower 0.5.1", + "tower", "tower-http", ] [[package]] name = "rsa" -version = "0.9.6" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" dependencies = [ "const-oid", "digest", @@ -2258,26 +1964,15 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.7.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "ring", - "rustls-webpki", - "sct", + "windows-sys 0.59.0", ] [[package]] @@ -2289,21 +1984,11 @@ dependencies = [ "base64 0.21.7", ] -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "ryu" @@ -2313,21 +1998,11 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" -dependencies = [ - "windows-sys 0.52.0", -] - -[[package]] -name = "scoped-futures" -version = "0.1.3" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1473e24c637950c9bd38763220bea91ec3e095a89f672bbd7a10d03e77ba467" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ - "cfg-if", - "pin-utils", + "windows-sys 0.59.0", ] [[package]] @@ -2336,23 +2011,13 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "security-framework" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.7.0", "core-foundation", "core-foundation-sys", "libc", @@ -2361,50 +2026,42 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", ] -[[package]] -name = "semver" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" -dependencies = [ - "serde", -] - [[package]] name = "serde" -version = "1.0.203" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.96", ] [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.135" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -2454,213 +2111,66 @@ dependencies = [ ] [[package]] -name = "sharded-slab" -version = "0.1.7" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "shuttle-axum" -version = "0.46.0" +name = "signature" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28f9f47b2d08a6c1c08032c8d0ec8ba45fed500cf807c90fd13a6b89fdcbc4d4" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "axum 0.7.5", - "shuttle-runtime", + "digest", + "rand_core", ] [[package]] -name = "shuttle-codegen" -version = "0.46.0" +name = "siphasher" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccc78593861ac3ed0a0efb2f59bc872c5fdf103ba4bb48a12b14a54d6ac98d20" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.68", -] +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] -name = "shuttle-common" -version = "0.46.0" +name = "slab" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1276703054c47dce9e0b5c18ed0beb7094dded4b94d9ebc5f1334ba7c21b33" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ - "anyhow", - "chrono", - "comfy-table", - "crossterm 0.27.0", - "http 0.2.12", - "opentelemetry", - "opentelemetry-http", - "pin-project", - "semver", - "serde", - "serde_json", - "strum 0.26.3", - "thiserror", - "tower 0.4.13", - "tracing", - "tracing-opentelemetry", - "tracing-subscriber", - "url", - "uuid", - "zeroize", + "autocfg", ] [[package]] -name = "shuttle-proto" -version = "0.46.0" +name = "smallvec" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c483a607932efd362ca1421e94019cae84ff786020ce341001e44e39975d8503" -dependencies = [ - "futures-core", - "prost", - "prost-types", - "shuttle-common", - "tonic", -] +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] -name = "shuttle-runtime" -version = "0.46.0" +name = "socket2" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d08612be826db7a7ba7c1d1f4cb594c45c7891a68fa50c300fea88ee5b9dbe4" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ - "anyhow", - "async-trait", - "colored", - "serde", - "serde_json", - "shuttle-codegen", - "shuttle-common", - "shuttle-proto", - "shuttle-service", - "strfmt", - "tokio", - "tokio-stream", - "tokio-util", - "tonic", - "tracing-subscriber", + "libc", + "windows-sys 0.52.0", ] [[package]] -name = "shuttle-service" -version = "0.46.0" +name = "spin" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca881c735af684acaab3dcaf9d6c2cc65e82636bfbe3d4afb3d666c1c3477e13" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" dependencies = [ - "anyhow", - "async-trait", - "serde", - "shuttle-common", - "strfmt", - "thiserror", + "lock_api", ] [[package]] -name = "shuttle-shared-db" -version = "0.46.0" +name = "spki" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccd902dc026098f2af1df364610fa8591daea3d5b15673339d1decddf0a8f9a6" -dependencies = [ - "async-trait", - "diesel-async", - "serde", - "serde_json", - "shuttle-service", - "sqlx", -] - -[[package]] -name = "signal-hook" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-mio" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" -dependencies = [ - "libc", - "mio", - "signal-hook", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core", -] - -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "socket2" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", @@ -2668,9 +2178,9 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" dependencies = [ "nom", "unicode_categories", @@ -2711,25 +2221,20 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.2.6", + "indexmap", "log", "memchr", "once_cell", "paste", "percent-encoding", - "rustls", - "rustls-pemfile", "serde", "serde_json", "sha2", "smallvec", "sqlformat", - "thiserror", - "tokio", - "tokio-stream", + "thiserror 1.0.69", "tracing", "url", - "webpki-roots", ] [[package]] @@ -2767,7 +2272,6 @@ dependencies = [ "sqlx-sqlite", "syn 1.0.109", "tempfile", - "tokio", "url", ] @@ -2779,7 +2283,7 @@ checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ "atoi", "base64 0.21.7", - "bitflags 2.5.0", + "bitflags 2.7.0", "byteorder", "bytes", "chrono", @@ -2809,7 +2313,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.69", "tracing", "whoami", ] @@ -2822,7 +2326,7 @@ checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" dependencies = [ "atoi", "base64 0.21.7", - "bitflags 2.5.0", + "bitflags 2.7.0", "byteorder", "chrono", "crc", @@ -2848,7 +2352,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.69", "tracing", "whoami", ] @@ -2878,16 +2382,16 @@ dependencies = [ ] [[package]] -name = "static_assertions_next" -version = "1.1.2" +name = "stable_deref_trait" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] -name = "strfmt" -version = "0.2.4" +name = "static_assertions_next" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a8348af2d9fc3258c8733b8d9d8db2e56f54b2363a4b5b81585c7875ed65e65" +checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766" [[package]] name = "stringprep" @@ -2906,32 +2410,13 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "strum" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" - [[package]] name = "strum" version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros 0.26.4", -] - -[[package]] -name = "strum_macros" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "rustversion", - "syn 1.0.109", + "strum_macros", ] [[package]] @@ -2944,7 +2429,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.68", + "syn 2.0.96", ] [[package]] @@ -2966,9 +2451,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.68" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -2983,9 +2468,20 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "sync_wrapper" -version = "1.0.1" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] [[package]] name = "system-configuration" @@ -3010,51 +2506,73 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.1" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", + "getrandom", + "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" -version = "1.0.61" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.11", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.96", ] [[package]] -name = "thread_local" -version = "1.1.8" +name = "thiserror-impl" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ - "cfg-if", - "once_cell", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", ] [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] @@ -3067,42 +2585,29 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", - "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", -] - -[[package]] -name = "tokio-io-timeout" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" -dependencies = [ - "pin-project-lite", - "tokio", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.96", ] [[package]] @@ -3115,37 +2620,11 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-postgres" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d340244b32d920260ae7448cb72b6e238bddc3d4f7603394e7dd46ed8e48f5b8" -dependencies = [ - "async-trait", - "byteorder", - "bytes", - "fallible-iterator", - "futures-channel", - "futures-util", - "log", - "parking_lot", - "percent-encoding", - "phf", - "pin-project-lite", - "postgres-protocol", - "postgres-types", - "rand", - "socket2", - "tokio", - "tokio-util", - "whoami", -] - [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -3154,9 +2633,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.21.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" dependencies = [ "futures-util", "log", @@ -3166,9 +2645,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -3189,87 +2668,46 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" -version = "0.21.1" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap 2.2.6", + "indexmap", "toml_datetime", "winnow", ] -[[package]] -name = "tonic" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" -dependencies = [ - "async-stream", - "async-trait", - "axum 0.6.20", - "base64 0.21.7", - "bytes", - "h2", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.29", - "hyper-timeout", - "percent-encoding", - "pin-project", - "prost", - "tokio", - "tokio-stream", - "tower 0.4.13", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower" -version = "0.4.13" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", - "indexmap 1.9.3", - "pin-project", "pin-project-lite", - "rand", - "slab", + "sync_wrapper 1.0.2", "tokio", - "tokio-util", "tower-layer", "tower-service", "tracing", ] -[[package]] -name = "tower" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" -dependencies = [ - "tower-layer", - "tower-service", -] - [[package]] name = "tower-http" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97" +checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.7.0", "bytes", - "http 1.1.0", + "http 1.2.0", "pin-project-lite", "tower-layer", "tower-service", @@ -3289,9 +2727,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -3301,83 +2739,22 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.96", ] [[package]] name = "tracing-core" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ - "log", "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-opentelemetry" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c67ac25c5407e7b961fafc6f7e9aa5958fd297aada2d20fa2ae1737357e55596" -dependencies = [ - "js-sys", - "once_cell", - "opentelemetry", - "opentelemetry_sdk", - "smallvec", - "tracing", - "tracing-core", - "tracing-log", - "tracing-subscriber", - "web-time", -] - -[[package]] -name = "tracing-serde" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" -dependencies = [ - "serde", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex", - "serde", - "serde_json", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", - "tracing-serde", ] [[package]] @@ -3388,20 +2765,19 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.21.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" dependencies = [ "byteorder", "bytes", "data-encoding", - "http 1.1.0", + "http 1.2.0", "httparse", "log", "rand", "sha1", - "thiserror", - "url", + "thiserror 1.0.69", "utf-8", ] @@ -3413,48 +2789,42 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ucd-trie" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" [[package]] name = "unicode-segmentation" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" - -[[package]] -name = "unicode-width" -version = "0.1.13" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode_categories" @@ -3462,22 +2832,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", - "serde", ] [[package]] @@ -3493,20 +2856,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] -name = "uuid" -version = "1.9.0" +name = "utf16_iter" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ea73390fe27785838dcbf75b91b1d84799e28f1ce71e6f372a5dc2200c80de5" -dependencies = [ - "getrandom", - "serde", -] +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" [[package]] -name = "valuable" -version = "0.1.0" +name = "utf8_iter" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "vcpkg" @@ -3516,9 +2875,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "want" @@ -3543,46 +2902,47 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.96", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3590,89 +2950,50 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.96", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "web-sys" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "0.2.4" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa30049b1c872b72c89866d458eae9f20380ab280ffd1b1e18df2d3e2d98cfe0" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" dependencies = [ "js-sys", "wasm-bindgen", ] -[[package]] -name = "webpki-roots" -version = "0.25.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" - [[package]] name = "whoami" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" dependencies = [ - "redox_syscall 0.4.1", + "redox_syscall", "wasite", - "web-sys", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", ] -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[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.5", + "windows-targets 0.52.6", ] [[package]] @@ -3690,7 +3011,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -3710,18 +3040,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -3732,9 +3062,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -3744,9 +3074,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -3756,15 +3086,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -3774,9 +3104,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -3786,9 +3116,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -3798,9 +3128,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -3810,15 +3140,15 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.5.40" +version = "0.6.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" dependencies = [ "memchr", ] @@ -3833,6 +3163,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "yaml-rust" version = "0.4.5" @@ -3842,24 +3184,70 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "synstructure", +] + [[package]] name = "zerocopy" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.96", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "synstructure", ] [[package]] @@ -3867,3 +3255,25 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] diff --git a/Cargo.toml b/Cargo.toml index d9384a1..a711c31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,6 @@ async-graphql-axum = "7.0.6" axum = "0.7.3" chrono = "0.4.38" serde = { version = "1.0.188", features = ["derive"] } -shuttle-axum = "0.46.0" -shuttle-runtime = "0.46.0" -shuttle-shared-db = { version = "0.46.0", features = ["postgres", "sqlx"] } sqlx = { version = "0.7.1", features = ["chrono"] } tokio = { version = "1.28.2", features = ["macros"] } # For async tests hmac = "0.12.1" diff --git a/src/main.rs b/src/main.rs index ad07d03..a85bc7e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,6 @@ use async_graphql_axum::GraphQL; use axum::{routing::get, Router}; use chrono::{Local, NaiveTime}; use root::attendance::scheduled_task::scheduled_task; -use shuttle_runtime::SecretStore; use sqlx::PgPool; use std::time::Duration; use std::{env, sync::Arc}; @@ -25,21 +24,16 @@ struct MyState { secret_key: String, } -//Main method -#[shuttle_runtime::main] -async fn main( - #[shuttle_shared_db::Postgres] pool: PgPool, - #[shuttle_runtime::Secrets] secrets: SecretStore, -) -> shuttle_axum::ShuttleAxum { +fn main() -> { + // TODO: Explain? env::set_var("PGOPTIONS", "-c ignore_version=true"); - sqlx::migrate!() - .run(&pool) - .await - .expect("Failed to run migrations"); + // sqlx::migrate!() + // .run(&pool) + // .await + // .expect("Failed to run migrations."); let pool = Arc::new(pool); - let secret_key = secrets.get("ROOT_SECRET").expect("ROOT_SECRET not found"); let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription) .data(pool.clone()) .data(secret_key.clone()) // @@ -65,7 +59,6 @@ async fn main( task::spawn(async move { schedule_task_at_midnight(pool.clone()).await; // Call the function after 10 seconds }); - Ok(router.into()) } //Ticker for calling the scheduled task From 3c01c3a74071521fffacd39370031b52b6edc0d8 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Sun, 12 Jan 2025 16:25:48 +0530 Subject: [PATCH 03/38] reduce codebase to basic working web server template --- Cargo.lock | 91 +++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 4 +- src/main.rs | 108 +++++++++++++++++++++++----------------------------- 3 files changed, 142 insertions(+), 61 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8efa54..d34900e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1452,6 +1452,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1568,6 +1578,12 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.12.3" @@ -1924,6 +1940,8 @@ dependencies = [ "tokio", "tower", "tower-http", + "tracing", + "tracing-subscriber", ] [[package]] @@ -2110,6 +2128,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2558,6 +2585,16 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -2755,6 +2792,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -2867,6 +2930,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" @@ -2987,6 +3056,28 @@ dependencies = [ "wasite", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index a711c31..cee987f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ axum = "0.7.3" chrono = "0.4.38" serde = { version = "1.0.188", features = ["derive"] } sqlx = { version = "0.7.1", features = ["chrono"] } -tokio = { version = "1.28.2", features = ["macros"] } # For async tests +tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } # For async tests hmac = "0.12.1" sha2 = "0.10.8" hex = "0.4.3" @@ -20,3 +20,5 @@ chrono-tz = "0.10.0" serde_json = "1.0" reqwest = { version = "0.11.27", features = ["json"] } config = "0.13" +tracing = "0.1.41" +tracing-subscriber = "0.3.19" diff --git a/src/main.rs b/src/main.rs index a85bc7e..5af4950 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,81 +1,69 @@ -use crate::graphql::mutations::MutationRoot; -use crate::graphql::query::QueryRoot; -use crate::routes::graphiql; -use async_graphql::{EmptySubscription, Schema}; -use async_graphql_axum::GraphQL; use axum::{routing::get, Router}; -use chrono::{Local, NaiveTime}; -use root::attendance::scheduled_task::scheduled_task; -use sqlx::PgPool; -use std::time::Duration; -use std::{env, sync::Arc}; -use tokio::task; -use tokio::time::{sleep_until, Instant}; use tower_http::cors::{Any, CorsLayer}; -mod db; -mod graphql; -mod leaderboard; -mod routes; +// #[derive(Clone)] +// struct MyState { +// pool: Arc, +// secret_key: String, +// } -#[derive(Clone)] -struct MyState { - pool: Arc, - secret_key: String, -} - -fn main() -> { - // TODO: Explain? - env::set_var("PGOPTIONS", "-c ignore_version=true"); +#[tokio::main] +async fn main() { + // 12/1/25: Going to assume this is only necessary for shuttle + // 9/1/25: TODO: Explain? + // env::set_var("PGOPTIONS", "-c ignore_version=true"); // sqlx::migrate!() // .run(&pool) // .await // .expect("Failed to run migrations."); - let pool = Arc::new(pool); - let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription) - .data(pool.clone()) - .data(secret_key.clone()) // - .finish(); + // let pool = Arc::new(pool); + // let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription) + // .data(pool.clone()) + // .data(secret_key.clone()) + // .finish(); - let state = MyState { - pool: pool.clone(), - secret_key: secret_key.clone(), - }; + // let state = MyState { + // pool: pool.clone(), + // secret_key: secret_key.clone(), + // }; + + tracing_subscriber::fmt::init(); let cors = CorsLayer::new() .allow_origin(Any) .allow_methods(tower_http::cors::Any) .allow_headers(tower_http::cors::Any); - let router = Router::new() - .route( - "/", - get(graphiql).post_service(GraphQL::new(schema.clone())), - ) - .with_state(state) - .layer(cors); - task::spawn(async move { - schedule_task_at_midnight(pool.clone()).await; // Call the function after 10 seconds - }); -} + let router = Router::new().route("/", get(root)).layer(cors); -//Ticker for calling the scheduled task -async fn schedule_task_at_midnight(pool: Arc) { - loop { - let now = Local::now(); + let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); + axum::serve(listener, router).await.unwrap(); - let tomorrow = now.date_naive().succ_opt().unwrap(); - let midnight = NaiveTime::from_hms_opt(0, 0, 0).unwrap(); - let next_midnight = tomorrow.and_time(midnight); - - let now_naive = now.naive_local(); - let duration_until_midnight = next_midnight.signed_duration_since(now_naive); - let sleep_duration = Duration::from_secs(duration_until_midnight.num_seconds() as u64 + 60); + // task::spawn(async move { + // schedule_task_at_midnight(pool.clone()).await; + // }); +} - sleep_until(Instant::now() + sleep_duration).await; - scheduled_task(pool.clone()).await; - print!("done"); - } +async fn root() -> &'static str { + "Hello, world!" } + +// Sleep till midnight, then execute the task, repeat. +// async fn schedule_task_at_midnight(pool: Arc) { +// loop { +// let now = Local::now(); +// let next_midnight = (now + chrono::Duration::days(1)) +// .date_naive() +// .and_hms_opt(0, 0, 0) +// .unwrap(); +// +// let duration_until_midnight = next_midnight.signed_duration_since(now.naive_local()); +// let sleep_duration = +// tokio::time::Duration::from_secs(duration_until_midnight.num_seconds() as u64); +// +// sleep_until(Instant::now() + sleep_duration).await; +// scheduled_task(pool.clone()).await; +// } +// } From c3c58d1131cb673b26b1f904ae37e288aa99cb7b Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Sun, 12 Jan 2025 16:26:03 +0530 Subject: [PATCH 04/38] remove migrations --- .../20240627145806_create_member_table.sql | 21 ------------ ...01161049_add_default_attendance_column.sql | 2 -- migrations/20240711150547_add_mac.sql | 1 - ...0813125650_rename_present_to_ispresent.sql | 1 - ...0813130838_rename_present_to_isPresent.sql | 1 - ...0813131821_rename_present_to_isPresent.sql | 1 - .../20241017154637_add_discord_id_field.sql | 1 - migrations/20241126123155_adding_streaks.sql | 2 -- .../20241126162522_add_member_group.sql | 1 - .../20241128224140_add_streak_relation.sql | 6 ---- ...9193435_remove_streks_from_membertable.sql | 3 -- .../20241129195135_add_streaks_table.sql | 6 ---- .../20241129195234_drop_old_streak_table.sql | 1 - .../20241129200327_drop_strek_update.sql | 1 - .../20241129200519_recreate_streaks.sql | 6 ---- migrations/20241201203531_leetcode_tables.sql | 33 ------------------- ...2102757_leetcode_tables_add_constraint.sql | 4 --- ...5211_add_attendance_strak_and_projects.sql | 14 -------- ...226074423_attendance_streak_constraint.sql | 1 - 19 files changed, 106 deletions(-) delete mode 100644 migrations/20240627145806_create_member_table.sql delete mode 100644 migrations/20240701161049_add_default_attendance_column.sql delete mode 100644 migrations/20240711150547_add_mac.sql delete mode 100644 migrations/20240813125650_rename_present_to_ispresent.sql delete mode 100644 migrations/20240813130838_rename_present_to_isPresent.sql delete mode 100644 migrations/20240813131821_rename_present_to_isPresent.sql delete mode 100644 migrations/20241017154637_add_discord_id_field.sql delete mode 100644 migrations/20241126123155_adding_streaks.sql delete mode 100644 migrations/20241126162522_add_member_group.sql delete mode 100644 migrations/20241128224140_add_streak_relation.sql delete mode 100644 migrations/20241129193435_remove_streks_from_membertable.sql delete mode 100644 migrations/20241129195135_add_streaks_table.sql delete mode 100644 migrations/20241129195234_drop_old_streak_table.sql delete mode 100644 migrations/20241129200327_drop_strek_update.sql delete mode 100644 migrations/20241129200519_recreate_streaks.sql delete mode 100644 migrations/20241201203531_leetcode_tables.sql delete mode 100644 migrations/20241202102757_leetcode_tables_add_constraint.sql delete mode 100644 migrations/20241226065211_add_attendance_strak_and_projects.sql delete mode 100644 migrations/20241226074423_attendance_streak_constraint.sql diff --git a/migrations/20240627145806_create_member_table.sql b/migrations/20240627145806_create_member_table.sql deleted file mode 100644 index 6bdf4c6..0000000 --- a/migrations/20240627145806_create_member_table.sql +++ /dev/null @@ -1,21 +0,0 @@ --- migrations/YYYYMMDDHHMMSS_create_member_table.sql - -CREATE TABLE Member ( - id SERIAL PRIMARY KEY, - rollno VARCHAR(20) NOT NULL, - name VARCHAR(255) NOT NULL, - hostel VARCHAR(255) NOT NULL, - email VARCHAR(255) NOT NULL UNIQUE, - sex VARCHAR(10) NOT NULL, - year INT NOT NULL, - created_at TIMESTAMP DEFAULT NOW() -); - -CREATE TABLE Attendance ( - id INT REFERENCES Member(id), - date DATE NOT NULL, - timein TIME NOT NULL, - timeout TIME NOT NULL, - PRIMARY KEY (id, date) -); - diff --git a/migrations/20240701161049_add_default_attendance_column.sql b/migrations/20240701161049_add_default_attendance_column.sql deleted file mode 100644 index e12563e..0000000 --- a/migrations/20240701161049_add_default_attendance_column.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE Attendance -ADD COLUMN present BOOLEAN DEFAULT FALSE; diff --git a/migrations/20240711150547_add_mac.sql b/migrations/20240711150547_add_mac.sql deleted file mode 100644 index 3808373..0000000 --- a/migrations/20240711150547_add_mac.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE Member ADD COLUMN macaddress TEXT; \ No newline at end of file diff --git a/migrations/20240813125650_rename_present_to_ispresent.sql b/migrations/20240813125650_rename_present_to_ispresent.sql deleted file mode 100644 index 79aab75..0000000 --- a/migrations/20240813125650_rename_present_to_ispresent.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE Attendance RENAME COLUMN present TO ispresent; diff --git a/migrations/20240813130838_rename_present_to_isPresent.sql b/migrations/20240813130838_rename_present_to_isPresent.sql deleted file mode 100644 index 06284bc..0000000 --- a/migrations/20240813130838_rename_present_to_isPresent.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE Attendance RENAME COLUMN ispresent TO is_prresent; diff --git a/migrations/20240813131821_rename_present_to_isPresent.sql b/migrations/20240813131821_rename_present_to_isPresent.sql deleted file mode 100644 index 6e2ff32..0000000 --- a/migrations/20240813131821_rename_present_to_isPresent.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE Attendance RENAME COLUMN is_prresent TO is_present; diff --git a/migrations/20241017154637_add_discord_id_field.sql b/migrations/20241017154637_add_discord_id_field.sql deleted file mode 100644 index c0e95cc..0000000 --- a/migrations/20241017154637_add_discord_id_field.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE Member ADD COLUMN discord_id TEXT; \ No newline at end of file diff --git a/migrations/20241126123155_adding_streaks.sql b/migrations/20241126123155_adding_streaks.sql deleted file mode 100644 index 11f8e60..0000000 --- a/migrations/20241126123155_adding_streaks.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE Member ADD COLUMN streak INT; -ALTER TABLE Member ADD COLUMN max_streak INT; \ No newline at end of file diff --git a/migrations/20241126162522_add_member_group.sql b/migrations/20241126162522_add_member_group.sql deleted file mode 100644 index f95c6e2..0000000 --- a/migrations/20241126162522_add_member_group.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE Member ADD COLUMN group_id INT; \ No newline at end of file diff --git a/migrations/20241128224140_add_streak_relation.sql b/migrations/20241128224140_add_streak_relation.sql deleted file mode 100644 index e42db4c..0000000 --- a/migrations/20241128224140_add_streak_relation.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE Streaks ( - id SERIAL PRIMARY KEY, - streak INT NOT NULL DEFAULT 0, - max_streak INT NOT NULL DEFAULT 0, - FOREIGN KEY (id) REFERENCES Member(id) ON DELETE CASCADE -); \ No newline at end of file diff --git a/migrations/20241129193435_remove_streks_from_membertable.sql b/migrations/20241129193435_remove_streks_from_membertable.sql deleted file mode 100644 index a147905..0000000 --- a/migrations/20241129193435_remove_streks_from_membertable.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE Member -DROP COLUMN streak, -DROP COLUMN max_streak; \ No newline at end of file diff --git a/migrations/20241129195135_add_streaks_table.sql b/migrations/20241129195135_add_streaks_table.sql deleted file mode 100644 index 36fe370..0000000 --- a/migrations/20241129195135_add_streaks_table.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE StreakUpdate ( - id SERIAL PRIMARY KEY, - streak INT NOT NULL DEFAULT 0, - max_streak INT NOT NULL DEFAULT 0, - FOREIGN KEY (id) REFERENCES Member(id) ON DELETE CASCADE -); \ No newline at end of file diff --git a/migrations/20241129195234_drop_old_streak_table.sql b/migrations/20241129195234_drop_old_streak_table.sql deleted file mode 100644 index 18f205d..0000000 --- a/migrations/20241129195234_drop_old_streak_table.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE Streaks; \ No newline at end of file diff --git a/migrations/20241129200327_drop_strek_update.sql b/migrations/20241129200327_drop_strek_update.sql deleted file mode 100644 index 4193851..0000000 --- a/migrations/20241129200327_drop_strek_update.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS StreakUpdate; \ No newline at end of file diff --git a/migrations/20241129200519_recreate_streaks.sql b/migrations/20241129200519_recreate_streaks.sql deleted file mode 100644 index 36fe370..0000000 --- a/migrations/20241129200519_recreate_streaks.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE StreakUpdate ( - id SERIAL PRIMARY KEY, - streak INT NOT NULL DEFAULT 0, - max_streak INT NOT NULL DEFAULT 0, - FOREIGN KEY (id) REFERENCES Member(id) ON DELETE CASCADE -); \ No newline at end of file diff --git a/migrations/20241201203531_leetcode_tables.sql b/migrations/20241201203531_leetcode_tables.sql deleted file mode 100644 index 1e5f2e5..0000000 --- a/migrations/20241201203531_leetcode_tables.sql +++ /dev/null @@ -1,33 +0,0 @@ -CREATE TABLE leaderboard ( - id SERIAL PRIMARY KEY, - member_id INT UNIQUE NOT NULL, - leetcode_score INT, - codeforces_score INT, - unified_score INT NOT NULL, - last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (member_id) REFERENCES member(id) -); - -CREATE TABLE leetcode_stats ( - id SERIAL PRIMARY KEY, - member_id INT NOT NULL, - leetcode_username VARCHAR(255) NOT NULL, - problems_solved INT NOT NULL, - easy_solved INT NOT NULL, - medium_solved INT NOT NULL, - hard_solved INT NOT NULL, - contests_participated INT NOT NULL, - best_rank INT NOT NULL, - total_contests INT NOT NULL, - FOREIGN KEY (member_id) REFERENCES member(id) -); - -CREATE TABLE codeforces_stats ( - id SERIAL PRIMARY KEY, - member_id INT NOT NULL, - codeforces_handle VARCHAR(255) NOT NULL, - codeforces_rating INT NOT NULL, - max_rating INT NOT NULL, - contests_participated INT NOT NULL, - FOREIGN KEY (member_id) REFERENCES member(id) -); diff --git a/migrations/20241202102757_leetcode_tables_add_constraint.sql b/migrations/20241202102757_leetcode_tables_add_constraint.sql deleted file mode 100644 index f41f71c..0000000 --- a/migrations/20241202102757_leetcode_tables_add_constraint.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE leetcode_stats -ADD CONSTRAINT leetcode_stats_member_id_key UNIQUE (member_id); -ALTER TABLE codeforces_stats -ADD CONSTRAINT codeforces_stats_member_id_key UNIQUE (member_id); diff --git a/migrations/20241226065211_add_attendance_strak_and_projects.sql b/migrations/20241226065211_add_attendance_strak_and_projects.sql deleted file mode 100644 index f075eb1..0000000 --- a/migrations/20241226065211_add_attendance_strak_and_projects.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE AttendanceStreak ( - id SERIAL PRIMARY KEY, - member_id INT NOT NULL, - month DATE, - streak INT NOT NULL DEFAULT 0, - CONSTRAINT fkey_member FOREIGN KEY (member_id) REFERENCES Member(id) ON DELETE CASCADE -); - -CREATE TABLE ActiveProjects ( - id SERIAL PRIMARY KEY, - member_id INT NOT NULL, - project_title TEXT, - CONSTRAINT fkey_member FOREIGN KEY (member_id) REFERENCES Member(id) ON DELETE CASCADE -); diff --git a/migrations/20241226074423_attendance_streak_constraint.sql b/migrations/20241226074423_attendance_streak_constraint.sql deleted file mode 100644 index 54ba0d8..0000000 --- a/migrations/20241226074423_attendance_streak_constraint.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE AttendanceStreak ADD CONSTRAINT unique_member_month UNIQUE (member_id, month); From 4f12588c5bbd237cee8369105aab3da814b66183 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Tue, 14 Jan 2025 12:38:38 +0530 Subject: [PATCH 05/38] add database backup to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index e8541fe..2a04909 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /target .shuttle-storage Secrets*.toml + +backups/ From cec9d58e6020fb57bdaedbdfcd46b507f7218a1d Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Tue, 14 Jan 2025 12:52:54 +0530 Subject: [PATCH 06/38] remove unused state from router --- src/main.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5af4950..5056735 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,6 @@ use axum::{routing::get, Router}; use tower_http::cors::{Any, CorsLayer}; -// #[derive(Clone)] -// struct MyState { -// pool: Arc, -// secret_key: String, -// } - #[tokio::main] async fn main() { // 12/1/25: Going to assume this is only necessary for shuttle @@ -18,17 +12,11 @@ async fn main() { // .await // .expect("Failed to run migrations."); - // let pool = Arc::new(pool); // let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription) // .data(pool.clone()) // .data(secret_key.clone()) // .finish(); - // let state = MyState { - // pool: pool.clone(), - // secret_key: secret_key.clone(), - // }; - tracing_subscriber::fmt::init(); let cors = CorsLayer::new() From d09e6a54c7404150aa769eaa507fec25a8e01200 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Tue, 14 Jan 2025 23:55:19 +0530 Subject: [PATCH 07/38] ignore .env files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2a04909..b01dd90 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ Secrets*.toml backups/ +.env From ec44d947630e3a62883cb861ae525afff7ba2420 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Tue, 14 Jan 2025 23:56:47 +0530 Subject: [PATCH 08/38] add migration to create initial tables for member, streaks and attendance --- Cargo.lock | 10 ++++ Cargo.toml | 3 +- migrations/20250114180047_create_tables.sql | 66 +++++++++++++++++++++ src/main.rs | 21 +++++-- 4 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 migrations/20250114180047_create_tables.sql diff --git a/Cargo.lock b/Cargo.lock index d34900e..398cd84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -591,6 +591,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "dotenvy" version = "0.15.7" @@ -1930,6 +1936,7 @@ dependencies = [ "chrono", "chrono-tz", "config", + "dotenv", "hex", "hmac", "reqwest", @@ -2260,6 +2267,8 @@ dependencies = [ "smallvec", "sqlformat", "thiserror 1.0.69", + "tokio", + "tokio-stream", "tracing", "url", ] @@ -2299,6 +2308,7 @@ dependencies = [ "sqlx-sqlite", "syn 1.0.109", "tempfile", + "tokio", "url", ] diff --git a/Cargo.toml b/Cargo.toml index cee987f..57f13f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ async-graphql-axum = "7.0.6" axum = "0.7.3" chrono = "0.4.38" serde = { version = "1.0.188", features = ["derive"] } -sqlx = { version = "0.7.1", features = ["chrono"] } +sqlx = { version = "0.7.1", features = ["chrono", "postgres", "runtime-tokio"] } tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } # For async tests hmac = "0.12.1" sha2 = "0.10.8" @@ -22,3 +22,4 @@ reqwest = { version = "0.11.27", features = ["json"] } config = "0.13" tracing = "0.1.41" tracing-subscriber = "0.3.19" +dotenv = "0.15.0" diff --git a/migrations/20250114180047_create_tables.sql b/migrations/20250114180047_create_tables.sql new file mode 100644 index 0000000..e91dcc6 --- /dev/null +++ b/migrations/20250114180047_create_tables.sql @@ -0,0 +1,66 @@ +-- Creates member, attendance, attendance_summary and streaks tables + +-- Custom type for sex +CREATE TYPE sex_type AS ENUM ('M', 'F', 'Other'); + +CREATE TABLE Member ( + member_id SERIAL PRIMARY KEY, + roll_no VARCHAR(16) NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + sex sex_type NOT NULL, + year INT NOT NULL, + hostel VARCHAR(255) NOT NULL, + mac_address VARCHAR(255) NOT NULL UNIQUE, + discord_id VARCHAR(255) NOT NULL UNIQUE, + group_id INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CHECK (year BETWEEN 1 and 4) +); + +CREATE TABLE Attendance ( + attendance_id SERIAL PRIMARY KEY, + member_id INT REFERENCES Member(member_id) ON DELETE CASCADE, + date DATE NOT NULL, + is_present BOOLEAN NOT NULL DEFAULT FALSE, + time_in TIME, + time_out TIME, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CHECK ( + (is_present = TRUE AND time_in IS NOT NULL AND time_out is NOT NULL) OR + (is_present = FALSE AND time_in IS NULL AND time_out IS NULL) + ), + CHECK (is_present = FALSE OR date <= CURRENT_DATE), + CHECK (time_out IS NULL OR time_out > time_in), + UNIQUE (member_id, date) +); + +CREATE OR REPLACE FUNCTION update_timestamp() +RETURNS TRIGGER AS +$$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER update_attendance_timestamp +BEFORE UPDATE ON Attendance +FOR EACH ROW +EXECUTE FUNCTION update_timestamp(); + +CREATE TABLE AttendanceSummary ( + member_id INT REFERENCES Member(member_id) ON DELETE CASCADE, + year INT NOT NULL, + month INT NOT NULL, + days_attended INT NOT NULL DEFAULT 0, + primary key (member_id, year, month) +); + +CREATE TABLE StatusUpdateStreaks ( + member_id INT REFERENCES Member(member_id) ON DELETE CASCADE, + current_streak int NOT NULL DEFAULT 0, + max_streak INT NOT NULL, + PRIMARY KEY (member_id) +); diff --git a/src/main.rs b/src/main.rs index 5056735..cc64c3f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use axum::{routing::get, Router}; use tower_http::cors::{Any, CorsLayer}; +use tracing::info; #[tokio::main] async fn main() { @@ -7,25 +8,33 @@ async fn main() { // 9/1/25: TODO: Explain? // env::set_var("PGOPTIONS", "-c ignore_version=true"); - // sqlx::migrate!() - // .run(&pool) - // .await - // .expect("Failed to run migrations."); - // let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription) // .data(pool.clone()) // .data(secret_key.clone()) // .finish(); tracing_subscriber::fmt::init(); + dotenv::dotenv().ok(); + + let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + let pool = sqlx::postgres::PgPoolOptions::new() + .max_connections(5) + .connect(&database_url) + .await + .expect("Pool must be initialized properly."); + + sqlx::migrate!() + .run(&pool) + .await + .expect("Failed to run migrations."); let cors = CorsLayer::new() .allow_origin(Any) .allow_methods(tower_http::cors::Any) .allow_headers(tower_http::cors::Any); + info!("Starting Root..."); let router = Router::new().route("/", get(root)).layer(cors); - let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, router).await.unwrap(); From bd5cd889bc8f0ca2e9e3017740fac3b91430f981 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Wed, 15 Jan 2025 00:32:57 +0530 Subject: [PATCH 09/38] remove shuttle workflows --- .github/workflows/deploy.yml | 38 ------------------------------- .github/workflows/shuttle-run.yml | 17 -------------- 2 files changed, 55 deletions(-) delete mode 100644 .github/workflows/deploy.yml delete mode 100644 .github/workflows/shuttle-run.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index ca37106..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Shuttle Deploy - -on: - push: - branches: - - master - workflow_dispatch: - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - name: Start PostgreSQL - run: | - docker run --name postgres-test -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=test_db -p 5432:5432 -d postgres:15 - - - name: Set TEST_DATABASE_URL - run: echo "TEST_DATABASE_URL=postgres://postgres:postgres@localhost:5432/test_db" >> $GITHUB_ENV - - - name: Install Rust and Cargo - run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - source $HOME/.cargo/env - - - name: Run Tests - run: cargo test - - - - - uses: shuttle-hq/deploy-action@main - with: - name: "root" - no-test: "true" - deploy-key: ${{ secrets.SHUTTLE_API_KEY }} - secrets: | - ROOT_SECRET = '${{ secrets.ROOT_SECRET }}' diff --git a/.github/workflows/shuttle-run.yml b/.github/workflows/shuttle-run.yml deleted file mode 100644 index ecbdb93..0000000 --- a/.github/workflows/shuttle-run.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: shuttle-run -on: - pull_request: - branches: [ "master", "develop" ] - -jobs: - run-project: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Run shuttle project locally. - uses: ivinjabraham/shuttle-run@v1.1 - with: - secrets: | - ROOT_SECRET = '${{ secrets.ROOT_SECRET }}' From 522ded8177d163f7f533cac2399deeee24c798f9 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Wed, 15 Jan 2025 10:14:57 +0530 Subject: [PATCH 10/38] rename db module to models in order to better reflect its contents --- src/attendance/scheduled_task.rs | 2 +- src/graphql/mutations.rs | 4 ++-- src/graphql/query.rs | 4 ++-- src/lib.rs | 4 ++-- src/{db => models}/attendance.rs | 0 src/{db => models}/leaderboard.rs | 0 src/{db => models}/member.rs | 0 src/{db => models}/mod.rs | 0 src/{db => models}/projects.rs | 0 9 files changed, 7 insertions(+), 7 deletions(-) rename src/{db => models}/attendance.rs (100%) rename src/{db => models}/leaderboard.rs (100%) rename src/{db => models}/member.rs (100%) rename src/{db => models}/mod.rs (100%) rename src/{db => models}/projects.rs (100%) diff --git a/src/attendance/scheduled_task.rs b/src/attendance/scheduled_task.rs index a49c8a4..3288e60 100644 --- a/src/attendance/scheduled_task.rs +++ b/src/attendance/scheduled_task.rs @@ -4,7 +4,7 @@ use sqlx::PgPool; use std::sync::Arc; use crate::{ - db::{ + models::{ leaderboard::{CodeforcesStats, LeetCodeStats}, member::Member, }, diff --git a/src/graphql/mutations.rs b/src/graphql/mutations.rs index 74a0d55..a52f5e9 100644 --- a/src/graphql/mutations.rs +++ b/src/graphql/mutations.rs @@ -11,7 +11,7 @@ use sha2::Sha256; type HmacSha256 = Hmac; -use crate::db::{attendance::Attendance, leaderboard::{CodeforcesStats, LeetCodeStats}, member::Member, member::StreakUpdate, projects::ActiveProjects}; +use crate::models::{attendance::Attendance, leaderboard::{CodeforcesStats, LeetCodeStats}, member::Member, member::StreakUpdate, projects::ActiveProjects}; pub struct MutationRoot; @@ -358,4 +358,4 @@ impl MutationRoot { Ok(active_project) } -} \ No newline at end of file +} diff --git a/src/graphql/query.rs b/src/graphql/query.rs index 65c5d76..1b24f05 100644 --- a/src/graphql/query.rs +++ b/src/graphql/query.rs @@ -1,9 +1,9 @@ use async_graphql::{Context, Object}; use chrono::NaiveDate; -use root::db::{attendance::{AttendanceStreak, AttendanceSummary, DailyCount, MemberAttendance}, projects::ActiveProjects}; +use root::models::{attendance::{AttendanceStreak, AttendanceSummary, DailyCount, MemberAttendance}, projects::ActiveProjects}; use sqlx::PgPool; use std::sync::Arc; -use crate::db::{ +use crate::models::{ attendance::Attendance, leaderboard::{CodeforcesStatsWithName, LeaderboardWithMember, LeetCodeStatsWithName}, member::{Member, StreakUpdate} }; diff --git a/src/lib.rs b/src/lib.rs index 2c08e96..a9b4b44 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -//need this to expose db module for tests -pub mod db; +// Need this to expose models module for tests +pub mod models; pub mod leaderboard; pub mod attendance; diff --git a/src/db/attendance.rs b/src/models/attendance.rs similarity index 100% rename from src/db/attendance.rs rename to src/models/attendance.rs diff --git a/src/db/leaderboard.rs b/src/models/leaderboard.rs similarity index 100% rename from src/db/leaderboard.rs rename to src/models/leaderboard.rs diff --git a/src/db/member.rs b/src/models/member.rs similarity index 100% rename from src/db/member.rs rename to src/models/member.rs diff --git a/src/db/mod.rs b/src/models/mod.rs similarity index 100% rename from src/db/mod.rs rename to src/models/mod.rs diff --git a/src/db/projects.rs b/src/models/projects.rs similarity index 100% rename from src/db/projects.rs rename to src/models/projects.rs From a16c83b544e501d450e14923551bb88b6e8785ab Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Wed, 15 Jan 2025 11:13:39 +0530 Subject: [PATCH 11/38] add graphql interface back to the server this commit should make the server unsuable since the logic for the queries/mutations is for the old schema. DO NOT RUN THIS COMMIT. --- src/attendance/scheduled_task.rs | 24 +++-- src/graphql/mutations.rs | 150 ++++++++++++++------------ src/graphql/query.rs | 104 +++++++++--------- src/leaderboard/mod.rs | 2 - src/leaderboard/update_leaderboard.rs | 5 +- src/lib.rs | 4 +- src/main.rs | 33 +++--- src/models/attendance.rs | 10 +- src/models/projects.rs | 4 +- src/routes.rs | 3 +- 10 files changed, 186 insertions(+), 153 deletions(-) diff --git a/src/attendance/scheduled_task.rs b/src/attendance/scheduled_task.rs index 3288e60..9b484b1 100644 --- a/src/attendance/scheduled_task.rs +++ b/src/attendance/scheduled_task.rs @@ -4,14 +4,14 @@ use sqlx::PgPool; use std::sync::Arc; use crate::{ - models::{ - leaderboard::{CodeforcesStats, LeetCodeStats}, - member::Member, - }, leaderboard::{ fetch_stats::{fetch_codeforces_stats, fetch_leetcode_stats}, update_leaderboard::update_leaderboard, }, + models::{ + leaderboard::{CodeforcesStats, LeetCodeStats}, + member::Member, + }, }; //Scheduled task for moving all members to Attendance table at midnight. pub async fn scheduled_task(pool: Arc) { @@ -57,7 +57,7 @@ pub async fn scheduled_task(pool: Arc) { if let Ok(Some(leetcode_stats)) = leetcode_username { let username = leetcode_stats.leetcode_username.clone(); - + // Fetch and update LeetCode stats match fetch_leetcode_stats(pool.clone(), member.id, &username).await { Ok(_) => println!("LeetCode stats updated for member ID: {}", member.id), @@ -104,8 +104,13 @@ pub async fn scheduled_task(pool: Arc) { // Function to update attendance streak async fn update_attendance_streak(member_id: i32, pool: &sqlx::PgPool) { - let today = chrono::Local::now().with_timezone(&chrono_tz::Asia::Kolkata).naive_local(); - let yesterday = today.checked_sub_signed(chrono::Duration::hours(12)).unwrap().date(); + let today = chrono::Local::now() + .with_timezone(&chrono_tz::Asia::Kolkata) + .naive_local(); + let yesterday = today + .checked_sub_signed(chrono::Duration::hours(12)) + .unwrap() + .date(); if today.day() == 1 { let _ = sqlx::query( @@ -176,6 +181,9 @@ async fn update_attendance_streak(member_id: i32, pool: &sqlx::PgPool) { println!("Sreak not incremented for member ID: {}", member_id); } Ok(_) => eprintln!("Unexpected attendance value for member ID: {}", member_id), - Err(e) => eprintln!("Error checking attendance for member ID {}: {:?}", member_id, e), + Err(e) => eprintln!( + "Error checking attendance for member ID {}: {:?}", + member_id, e + ), } } diff --git a/src/graphql/mutations.rs b/src/graphql/mutations.rs index a52f5e9..beab8a3 100644 --- a/src/graphql/mutations.rs +++ b/src/graphql/mutations.rs @@ -1,41 +1,44 @@ -use async_graphql::{Context, Object}; use ::chrono::Local; +use async_graphql::{Context, Object}; use chrono::{NaiveDate, NaiveTime}; use chrono_tz::Asia::Kolkata; -use sqlx::PgPool; +use hmac::{Hmac, Mac}; +use sha2::Sha256; use sqlx::types::chrono; +use sqlx::PgPool; use std::sync::Arc; -use hmac::{Hmac,Mac}; -use sha2::Sha256; - type HmacSha256 = Hmac; -use crate::models::{attendance::Attendance, leaderboard::{CodeforcesStats, LeetCodeStats}, member::Member, member::StreakUpdate, projects::ActiveProjects}; +use root::models::{ + attendance::Attendance, + leaderboard::{CodeforcesStats, LeetCodeStats}, + member::Member, + member::StreakUpdate, + projects::ActiveProjects, +}; pub struct MutationRoot; #[Object] impl MutationRoot { - //Mutation for adding members to the Member table async fn add_member( - &self, - ctx: &Context<'_>, - rollno: String, - name: String, - hostel: String, - email: String, - sex: String, + &self, + ctx: &Context<'_>, + rollno: String, + name: String, + hostel: String, + email: String, + sex: String, year: i32, macaddress: String, discord_id: String, group_id: i32, - ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); - - + let pool = ctx + .data::>() + .expect("Pool not found in context"); let member = sqlx::query_as::<_, Member>( "INSERT INTO Member (rollno, name, hostel, email, sex, year, macaddress, discord_id, group_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *" @@ -55,7 +58,6 @@ impl MutationRoot { Ok(member) } - async fn edit_member( &self, ctx: &Context<'_>, @@ -66,25 +68,31 @@ impl MutationRoot { discord_id: String, group_id: i32, hmac_signature: String, - ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); - - let secret_key = ctx.data::().expect("HMAC secret not found in context"); + ) -> Result { + let pool = ctx + .data::>() + .expect("Pool not found in context"); + + let secret_key = ctx + .data::() + .expect("HMAC secret not found in context"); - let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes()).expect("HMAC can take key of any size"); + let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes()) + .expect("HMAC can take key of any size"); - let message = format!("{}{}{}{}{}{}", id, hostel, year, macaddress, discord_id, group_id); + let message = format!( + "{}{}{}{}{}{}", + id, hostel, year, macaddress, discord_id, group_id + ); mac.update(message.as_bytes()); let expected_signature = mac.finalize().into_bytes(); - + // Convert the received HMAC signature from the client to bytes for comparison let received_signature = hex::decode(hmac_signature) .map_err(|_| sqlx::Error::Protocol("Invalid HMAC signature".into()))?; - if expected_signature.as_slice() != received_signature.as_slice() { - return Err(sqlx::Error::Protocol("HMAC verification failed".into())); } @@ -99,9 +107,8 @@ impl MutationRoot { group_id = CASE WHEN $5 = 0 THEN group_id ELSE $5 END WHERE id = $6 RETURNING * - " + ", ) - .bind(hostel) .bind(year) .bind(macaddress) @@ -113,27 +120,25 @@ impl MutationRoot { Ok(member) } - + //Mutation for adding attendance to the Attendance table async fn add_attendance( - &self, - + ctx: &Context<'_>, id: i32, date: NaiveDate, timein: NaiveTime, timeout: NaiveTime, is_present: bool, - ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); - + let pool = ctx + .data::>() + .expect("Pool not found in context"); let attendance = sqlx::query_as::<_, Attendance>( "INSERT INTO Attendance (id, date, timein, timeout, is_present) VALUES ($1, $2, $3, $4, $5) RETURNING *" ) - .bind(id) .bind(date) .bind(timein) @@ -144,34 +149,36 @@ impl MutationRoot { Ok(attendance) } - + async fn mark_attendance( &self, ctx: &Context<'_>, id: i32, date: NaiveDate, is_present: bool, - hmac_signature: String, - ) -> Result { - - let pool = ctx.data::>().expect("Pool not found in context"); + hmac_signature: String, + ) -> Result { + let pool = ctx + .data::>() + .expect("Pool not found in context"); - let secret_key = ctx.data::().expect("HMAC secret not found in context"); + let secret_key = ctx + .data::() + .expect("HMAC secret not found in context"); - let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes()).expect("HMAC can take key of any size"); + let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes()) + .expect("HMAC can take key of any size"); let message = format!("{}{}{}", id, date, is_present); mac.update(message.as_bytes()); let expected_signature = mac.finalize().into_bytes(); - + // Convert the received HMAC signature from the client to bytes for comparison let received_signature = hex::decode(hmac_signature) .map_err(|_| sqlx::Error::Protocol("Invalid HMAC signature".into()))?; - if expected_signature.as_slice() != received_signature.as_slice() { - return Err(sqlx::Error::Protocol("HMAC verification failed".into())); } @@ -186,7 +193,7 @@ impl MutationRoot { is_present = $2 WHERE id = $3 AND date = $4 RETURNING * - " + ", ) .bind(current_time) .bind(is_present) @@ -199,14 +206,16 @@ impl MutationRoot { } //here when user changes the handle, it just updates the handle in the database without updating the other values till midnight - + async fn add_or_update_leetcode_username( &self, ctx: &Context<'_>, member_id: i32, username: String, ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); + let pool = ctx + .data::>() + .expect("Pool not found in context"); let result = sqlx::query_as::<_, LeetCodeStats>( " @@ -231,7 +240,9 @@ impl MutationRoot { member_id: i32, handle: String, ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); + let pool = ctx + .data::>() + .expect("Pool not found in context"); let result = sqlx::query_as::<_, CodeforcesStats>( " @@ -255,20 +266,22 @@ impl MutationRoot { id: i32, has_sent_update: bool, ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); + let pool = ctx + .data::>() + .expect("Pool not found in context"); let streak_info = sqlx::query_as::<_, StreakUpdate>( " SELECT id, streak, max_streak FROM StreakUpdate WHERE id = $1 - " + ", ) .bind(id) .fetch_optional(pool.as_ref()) .await?; - match streak_info{ + match streak_info { Some(mut member) => { let current_streak = member.streak.unwrap_or(0); let max_streak = member.max_streak.unwrap_or(0); @@ -285,7 +298,7 @@ impl MutationRoot { SET streak = $1, max_streak = $2 WHERE id = $3 RETURNING * - " + ", ) .bind(new_streak) .bind(new_max_streak) @@ -294,14 +307,14 @@ impl MutationRoot { .await?; Ok(updated_member) - }, + } None => { let new_member = sqlx::query_as::<_, StreakUpdate>( " INSERT INTO StreakUpdate (id, streak, max_streak) VALUES ($1, $2, $3) RETURNING * - " + ", ) .bind(id) .bind(0) @@ -318,16 +331,18 @@ impl MutationRoot { &self, ctx: &Context<'_>, id: i32, - project_name:String, - ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); + project_name: String, + ) -> Result { + let pool = ctx + .data::>() + .expect("Pool not found in context"); - let active_project = sqlx::query_as::<_,ActiveProjects>( + let active_project = sqlx::query_as::<_, ActiveProjects>( " INSERT INTO ActiveProjects (member_id,project_title) VALUES ($1,$2) RETURNING * - " + ", ) .bind(id) .bind(project_name) @@ -341,15 +356,17 @@ impl MutationRoot { &self, ctx: &Context<'_>, project_id: i32, - ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); + ) -> Result { + let pool = ctx + .data::>() + .expect("Pool not found in context"); - let active_project = sqlx::query_as::<_,ActiveProjects>( + let active_project = sqlx::query_as::<_, ActiveProjects>( " DELETE FROM ActiveProjects WHERE id = $1 RETURNING * - " + ", ) .bind(project_id) .fetch_one(pool.as_ref()) @@ -357,5 +374,4 @@ impl MutationRoot { Ok(active_project) } - } diff --git a/src/graphql/query.rs b/src/graphql/query.rs index 1b24f05..6464315 100644 --- a/src/graphql/query.rs +++ b/src/graphql/query.rs @@ -1,11 +1,16 @@ use async_graphql::{Context, Object}; use chrono::NaiveDate; -use root::models::{attendance::{AttendanceStreak, AttendanceSummary, DailyCount, MemberAttendance}, projects::ActiveProjects}; +use root::models::{ + attendance::Attendance, + leaderboard::{CodeforcesStatsWithName, LeaderboardWithMember, LeetCodeStatsWithName}, + member::{Member, StreakUpdate}, +}; +use root::models::{ + attendance::{AttendanceStreak, AttendanceSummary, DailyCount, MemberAttendance}, + projects::ActiveProjects, +}; use sqlx::PgPool; use std::sync::Arc; -use crate::models::{ - attendance::Attendance, leaderboard::{CodeforcesStatsWithName, LeaderboardWithMember, LeetCodeStatsWithName}, member::{Member, StreakUpdate} -}; pub struct QueryRoot; @@ -97,43 +102,42 @@ impl QueryRoot { .await?; Ok(attendance_list) } - async fn get_streak( - &self, - ctx: &Context<'_>, - id: i32, - ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); + async fn get_streak(&self, ctx: &Context<'_>, id: i32) -> Result { + let pool = ctx + .data::>() + .expect("Pool not found in context"); let streak = sqlx::query_as::<_, StreakUpdate>("SELECT * FROM StreakUpdate WHERE id = $1") - .bind(id) - .fetch_one(pool.as_ref()) - .await?; + .bind(id) + .fetch_one(pool.as_ref()) + .await?; Ok(streak) } - async fn get_update_streak( - &self, - ctx: &Context<'_>, - ) -> Result, sqlx::Error> { - let pool = ctx.data::>().expect("Pool not found in context"); + async fn get_update_streak(&self, ctx: &Context<'_>) -> Result, sqlx::Error> { + let pool = ctx + .data::>() + .expect("Pool not found in context"); let streak = sqlx::query_as::<_, StreakUpdate>("SELECT * FROM StreakUpdate") - .fetch_all(pool.as_ref()) - .await?; + .fetch_all(pool.as_ref()) + .await?; Ok(streak) } - + async fn get_attendance_streak( &self, ctx: &Context<'_>, start_date: NaiveDate, end_date: NaiveDate, ) -> Result, sqlx::Error> { - let pool = ctx.data::>().expect("Pool not found in context"); - let attendance_streak = sqlx::query_as::<_,AttendanceStreak>( + let pool = ctx + .data::>() + .expect("Pool not found in context"); + let attendance_streak = sqlx::query_as::<_, AttendanceStreak>( "SELECT * from AttendanceStreak WHERE month >= $1 AND month < $2 - " + ", ) .bind(start_date) .bind(end_date) @@ -148,13 +152,15 @@ impl QueryRoot { ctx: &Context<'_>, start_date: NaiveDate, end_date: NaiveDate, - ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); + ) -> Result { + let pool = ctx + .data::>() + .expect("Pool not found in context"); let attendance_days = sqlx::query_as::<_, (NaiveDate, i64)>( "SELECT date, COUNT(*) FROM Attendance WHERE date >= $1 AND date < $2 AND is_present = true - GROUP BY date ORDER BY date" + GROUP BY date ORDER BY date", ) .bind(start_date) .bind(end_date) @@ -165,7 +171,7 @@ impl QueryRoot { "SELECT id, COUNT(*) FROM Attendance WHERE date >= $1 AND date < $2 AND is_present = true - GROUP BY id" + GROUP BY id", ) .bind(start_date) .bind(end_date) @@ -175,7 +181,7 @@ impl QueryRoot { let max_count = sqlx::query_scalar::<_, i64>( "SELECT COUNT(*) FROM Attendance WHERE date >= $1 AND date < $2 - AND is_present = true" + AND is_present = true", ) .bind(start_date) .bind(end_date) @@ -183,18 +189,16 @@ impl QueryRoot { .await?; let daily_count = attendance_days - .into_iter().map(|(date, count)| DailyCount{ - date, count - }) + .into_iter() + .map(|(date, count)| DailyCount { date, count }) .collect(); let member_attendance = member_attendance - .into_iter().map(|(id, present_days)| MemberAttendance{ - id, present_days - }) + .into_iter() + .map(|(id, present_days)| MemberAttendance { id, present_days }) .collect(); - let summaries: AttendanceSummary = AttendanceSummary{ + let summaries: AttendanceSummary = AttendanceSummary { max_days: max_count[0], member_attendance, daily_count, @@ -207,33 +211,35 @@ impl QueryRoot { &self, ctx: &Context<'_>, ) -> Result, sqlx::Error> { - let pool = ctx.data::>().expect("Pool not found in context"); - + let pool = ctx + .data::>() + .expect("Pool not found in context"); + let dates = sqlx::query_scalar::<_, NaiveDate>( "SELECT date FROM Attendance GROUP BY date HAVING BOOL_AND(NOT is_present) - ORDER BY date" + ORDER BY date", ) .fetch_all(pool.as_ref()) .await?; - + Ok(dates) } - + pub async fn get_projects( &self, ctx: &Context<'_>, ) -> Result, sqlx::Error> { - let pool = ctx.data::>().expect("Pool not found in context"); - - let active_projects = sqlx::query_as::<_, ActiveProjects>( - "SELECT * FROM ActiveProjects" - ) - .fetch_all(pool.as_ref()) - .await?; - + let pool = ctx + .data::>() + .expect("Pool not found in context"); + + let active_projects = sqlx::query_as::<_, ActiveProjects>("SELECT * FROM ActiveProjects") + .fetch_all(pool.as_ref()) + .await?; + Ok(active_projects) } } diff --git a/src/leaderboard/mod.rs b/src/leaderboard/mod.rs index 7d7eadf..5dbae36 100644 --- a/src/leaderboard/mod.rs +++ b/src/leaderboard/mod.rs @@ -1,4 +1,2 @@ pub mod fetch_stats; pub mod update_leaderboard; - - diff --git a/src/leaderboard/update_leaderboard.rs b/src/leaderboard/update_leaderboard.rs index e8bf348..f575957 100644 --- a/src/leaderboard/update_leaderboard.rs +++ b/src/leaderboard/update_leaderboard.rs @@ -1,8 +1,7 @@ -use std::sync::Arc; use sqlx::PgPool; +use std::sync::Arc; pub async fn update_leaderboard(pool: Arc) -> Result<(), Box> { - let leetcode_stats: Result, _> = sqlx::query_as::<_, (i32, i32, i32, i32, i32, i32, i32, i32)>( "SELECT id, member_id, problems_solved, easy_solved, medium_solved, hard_solved, contests_participated, best_rank @@ -19,7 +18,6 @@ pub async fn update_leaderboard(pool: Arc) -> Result<(), Box, _> = sqlx::query_as::<_, (i32, i32, i32, i32, i32)>( "SELECT id, member_id, codeforces_rating, max_rating, contests_participated @@ -49,7 +47,6 @@ pub async fn update_leaderboard(pool: Arc) -> Result<(), Box &'static str { - "Hello, world!" -} - // Sleep till midnight, then execute the task, repeat. // async fn schedule_task_at_midnight(pool: Arc) { // loop { diff --git a/src/models/attendance.rs b/src/models/attendance.rs index f03da67..4aa7f77 100644 --- a/src/models/attendance.rs +++ b/src/models/attendance.rs @@ -1,6 +1,6 @@ +use async_graphql::SimpleObject; use chrono::{NaiveDate, NaiveTime}; use sqlx::FromRow; -use async_graphql::SimpleObject; //Struct for the Attendance table #[derive(FromRow, SimpleObject)] @@ -22,19 +22,19 @@ pub struct AttendanceStreak { #[derive(FromRow, SimpleObject)] pub struct AttendanceSummary { - pub max_days:i64, + pub max_days: i64, pub member_attendance: Vec, pub daily_count: Vec, } #[derive(FromRow, SimpleObject)] pub struct MemberAttendance { - pub id:i32, - pub present_days:i64, + pub id: i32, + pub present_days: i64, } #[derive(FromRow, SimpleObject)] pub struct DailyCount { pub date: NaiveDate, pub count: i64, -} \ No newline at end of file +} diff --git a/src/models/projects.rs b/src/models/projects.rs index d0f930f..448c5e5 100644 --- a/src/models/projects.rs +++ b/src/models/projects.rs @@ -1,9 +1,9 @@ -use sqlx::FromRow; use async_graphql::SimpleObject; +use sqlx::FromRow; #[derive(FromRow, SimpleObject)] pub struct ActiveProjects { id: i32, member_id: i32, project_title: Option, -} \ No newline at end of file +} diff --git a/src/routes.rs b/src/routes.rs index 1dfdeac..f94d6b3 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,6 +1,5 @@ - -use axum::response::{Html, IntoResponse}; use async_graphql::http::GraphiQLSource; +use axum::response::{Html, IntoResponse}; pub async fn graphiql() -> impl IntoResponse { Html( From e8d341a735ccdf3d169a6479332af9564157f033 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Wed, 15 Jan 2025 11:15:27 +0530 Subject: [PATCH 12/38] minor refinements to loading env variables, db pool configuration and CORSLayer --- src/main.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index f2945d0..74c8524 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,12 +16,17 @@ async fn main() { // env::set_var("PGOPTIONS", "-c ignore_version=true"); tracing_subscriber::fmt::init(); - dotenv::dotenv().ok(); + // Currently, we need the DATABASE_URL to be loaded in through the .env. + // In the future, if we use any other configuration (say Github Secrets), we + // can allow dotenv() to err. + dotenv::dotenv().expect("Failed to load .env file."); let secret_key = std::env::var("ROOT_SECRET").expect("ROOT_SECRET must be set."); let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set."); let pool = sqlx::postgres::PgPoolOptions::new() - .max_connections(5) + .min_connections(2) // Maintain at least two connections, one for amD and one for Home + .max_connections(3) // It should be pretty unlikely that amD, Home and the web interface is + // used simultaneously .connect(&database_url) .await .expect("Pool must be initialized properly."); @@ -37,8 +42,8 @@ async fn main() { .finish(); let cors = CorsLayer::new() - .allow_origin(Any) - .allow_methods(tower_http::cors::Any) + .allow_origin(HeaderValue::from_static("https://home.amfoss.in")) // Only allow requests from Home + .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) .allow_headers(tower_http::cors::Any); info!("Starting Root..."); From e7f43c721925346d1723bc745508c868066f494a Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Wed, 15 Jan 2025 13:17:28 +0530 Subject: [PATCH 13/38] uncomment scheduled task --- src/main.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main.rs b/src/main.rs index 74c8524..7098450 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use async_graphql_axum::GraphQL; use axum::{http::{HeaderValue, Method}, routing::get, Router}; use graphql::{mutations::MutationRoot, query::QueryRoot}; use tower_http::cors::{Any, CorsLayer}; +use std::sync::Arc; use tracing::info; mod graphql; @@ -24,9 +25,8 @@ async fn main() { let secret_key = std::env::var("ROOT_SECRET").expect("ROOT_SECRET must be set."); let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set."); let pool = sqlx::postgres::PgPoolOptions::new() - .min_connections(2) // Maintain at least two connections, one for amD and one for Home - .max_connections(3) // It should be pretty unlikely that amD, Home and the web interface is - // used simultaneously + .min_connections(2) // Maintain at least two connections, one for amD and one for Home. It should be + .max_connections(3) // pretty unlikely that amD, Home and the web interface is used simultaneously .connect(&database_url) .await .expect("Pool must be initialized properly."); @@ -36,11 +36,18 @@ async fn main() { .await .expect("Failed to run migrations."); + // Wrap pool in an Arc to share across threads + let pool = Arc::new(pool); + let schema = async_graphql::Schema::build(QueryRoot, MutationRoot, EmptySubscription) - .data(pool) + .data(pool.clone()) .data(secret_key) .finish(); + tokio::task::spawn(async { + schedule_task_at_midnight(pool).await; + }); + let cors = CorsLayer::new() .allow_origin(HeaderValue::from_static("https://home.amfoss.in")) // Only allow requests from Home .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) @@ -55,10 +62,6 @@ async fn main() { .layer(cors); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, router).await.unwrap(); - - // task::spawn(async move { - // schedule_task_at_midnight(pool.clone()).await; - // }); } // Sleep till midnight, then execute the task, repeat. From 7ca67ea4f85b568f552e2fea8f1035b1bf2298b5 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Wed, 15 Jan 2025 13:19:14 +0530 Subject: [PATCH 14/38] rename scheduled_task to daily_task --- .../{scheduled_task.rs => daily_task.rs} | 26 ++++------- src/attendance/mod.rs | 2 +- src/main.rs | 46 +++++++++++-------- 3 files changed, 36 insertions(+), 38 deletions(-) rename src/attendance/{scheduled_task.rs => daily_task.rs} (93%) diff --git a/src/attendance/scheduled_task.rs b/src/attendance/daily_task.rs similarity index 93% rename from src/attendance/scheduled_task.rs rename to src/attendance/daily_task.rs index 9b484b1..d3c8926 100644 --- a/src/attendance/scheduled_task.rs +++ b/src/attendance/daily_task.rs @@ -4,17 +4,17 @@ use sqlx::PgPool; use std::sync::Arc; use crate::{ + db::{ + leaderboard::{CodeforcesStats, LeetCodeStats}, + member::Member, + }, leaderboard::{ fetch_stats::{fetch_codeforces_stats, fetch_leetcode_stats}, update_leaderboard::update_leaderboard, }, - models::{ - leaderboard::{CodeforcesStats, LeetCodeStats}, - member::Member, - }, }; //Scheduled task for moving all members to Attendance table at midnight. -pub async fn scheduled_task(pool: Arc) { +pub async fn execute_daily_task(pool: Arc) { let members: Result, sqlx::Error> = sqlx::query_as::<_, Member>("SELECT * FROM Member") .fetch_all(pool.as_ref()) @@ -57,7 +57,7 @@ pub async fn scheduled_task(pool: Arc) { if let Ok(Some(leetcode_stats)) = leetcode_username { let username = leetcode_stats.leetcode_username.clone(); - + // Fetch and update LeetCode stats match fetch_leetcode_stats(pool.clone(), member.id, &username).await { Ok(_) => println!("LeetCode stats updated for member ID: {}", member.id), @@ -104,13 +104,8 @@ pub async fn scheduled_task(pool: Arc) { // Function to update attendance streak async fn update_attendance_streak(member_id: i32, pool: &sqlx::PgPool) { - let today = chrono::Local::now() - .with_timezone(&chrono_tz::Asia::Kolkata) - .naive_local(); - let yesterday = today - .checked_sub_signed(chrono::Duration::hours(12)) - .unwrap() - .date(); + let today = chrono::Local::now().with_timezone(&chrono_tz::Asia::Kolkata).naive_local(); + let yesterday = today.checked_sub_signed(chrono::Duration::hours(12)).unwrap().date(); if today.day() == 1 { let _ = sqlx::query( @@ -181,9 +176,6 @@ async fn update_attendance_streak(member_id: i32, pool: &sqlx::PgPool) { println!("Sreak not incremented for member ID: {}", member_id); } Ok(_) => eprintln!("Unexpected attendance value for member ID: {}", member_id), - Err(e) => eprintln!( - "Error checking attendance for member ID {}: {:?}", - member_id, e - ), + Err(e) => eprintln!("Error checking attendance for member ID {}: {:?}", member_id, e), } } diff --git a/src/attendance/mod.rs b/src/attendance/mod.rs index 66066d6..884e79f 100644 --- a/src/attendance/mod.rs +++ b/src/attendance/mod.rs @@ -1 +1 @@ -pub mod scheduled_task; +pub mod daily_task; diff --git a/src/main.rs b/src/main.rs index 7098450..1643f0d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,17 @@ use crate::routes::graphiql; use async_graphql::EmptySubscription; use async_graphql_axum::GraphQL; -use axum::{http::{HeaderValue, Method}, routing::get, Router}; +use axum::{ + http::{HeaderValue, Method}, + routing::get, + Router, +}; use graphql::{mutations::MutationRoot, query::QueryRoot}; -use tower_http::cors::{Any, CorsLayer}; +use root::attendance::daily_task; +use sqlx::PgPool; use std::sync::Arc; +use tokio::time::sleep_until; +use tower_http::cors::CorsLayer; use tracing::info; mod graphql; @@ -45,7 +52,7 @@ async fn main() { .finish(); tokio::task::spawn(async { - schedule_task_at_midnight(pool).await; + run_daily_task_at_midnight(pool).await; }); let cors = CorsLayer::new() @@ -64,20 +71,19 @@ async fn main() { axum::serve(listener, router).await.unwrap(); } -// Sleep till midnight, then execute the task, repeat. -// async fn schedule_task_at_midnight(pool: Arc) { -// loop { -// let now = Local::now(); -// let next_midnight = (now + chrono::Duration::days(1)) -// .date_naive() -// .and_hms_opt(0, 0, 0) -// .unwrap(); -// -// let duration_until_midnight = next_midnight.signed_duration_since(now.naive_local()); -// let sleep_duration = -// tokio::time::Duration::from_secs(duration_until_midnight.num_seconds() as u64); -// -// sleep_until(Instant::now() + sleep_duration).await; -// scheduled_task(pool.clone()).await; -// } -// } +async fn run_daily_task_at_midnight(pool: Arc) { + loop { + let now = chrono::Local::now(); + let next_midnight = (now + chrono::Duration::days(1)) + .date_naive() + .and_hms_opt(0, 0, 0) + .unwrap(); + + let duration_until_midnight = next_midnight.signed_duration_since(now.naive_local()); + let sleep_duration = + tokio::time::Duration::from_secs(duration_until_midnight.num_seconds() as u64); + + sleep_until(tokio::time::Instant::now() + sleep_duration).await; + daily_task::execute_daily_task(pool.clone()).await; + } +} From b3f1842411532c35c860ac44b21b7217c6a7734b Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Wed, 15 Jan 2025 13:22:45 +0530 Subject: [PATCH 15/38] rename reference to db module to models, was left out earlier --- src/attendance/daily_task.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/attendance/daily_task.rs b/src/attendance/daily_task.rs index d3c8926..034c389 100644 --- a/src/attendance/daily_task.rs +++ b/src/attendance/daily_task.rs @@ -4,7 +4,7 @@ use sqlx::PgPool; use std::sync::Arc; use crate::{ - db::{ + models::{ leaderboard::{CodeforcesStats, LeetCodeStats}, member::Member, }, @@ -57,7 +57,7 @@ pub async fn execute_daily_task(pool: Arc) { if let Ok(Some(leetcode_stats)) = leetcode_username { let username = leetcode_stats.leetcode_username.clone(); - + // Fetch and update LeetCode stats match fetch_leetcode_stats(pool.clone(), member.id, &username).await { Ok(_) => println!("LeetCode stats updated for member ID: {}", member.id), @@ -104,8 +104,13 @@ pub async fn execute_daily_task(pool: Arc) { // Function to update attendance streak async fn update_attendance_streak(member_id: i32, pool: &sqlx::PgPool) { - let today = chrono::Local::now().with_timezone(&chrono_tz::Asia::Kolkata).naive_local(); - let yesterday = today.checked_sub_signed(chrono::Duration::hours(12)).unwrap().date(); + let today = chrono::Local::now() + .with_timezone(&chrono_tz::Asia::Kolkata) + .naive_local(); + let yesterday = today + .checked_sub_signed(chrono::Duration::hours(12)) + .unwrap() + .date(); if today.day() == 1 { let _ = sqlx::query( @@ -176,6 +181,9 @@ async fn update_attendance_streak(member_id: i32, pool: &sqlx::PgPool) { println!("Sreak not incremented for member ID: {}", member_id); } Ok(_) => eprintln!("Unexpected attendance value for member ID: {}", member_id), - Err(e) => eprintln!("Error checking attendance for member ID {}: {:?}", member_id, e), + Err(e) => eprintln!( + "Error checking attendance for member ID {}: {:?}", + member_id, e + ), } } From 6201d06c5ae4fa9c26dc719d94cfbedf7b9952f3 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Wed, 15 Jan 2025 15:45:35 +0530 Subject: [PATCH 16/38] use kolkata timezone instead of local time --- src/main.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 1643f0d..23b26ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use axum::{ routing::get, Router, }; +use chrono_tz::Asia::Kolkata; use graphql::{mutations::MutationRoot, query::QueryRoot}; use root::attendance::daily_task; use sqlx::PgPool; @@ -51,6 +52,8 @@ async fn main() { .data(secret_key) .finish(); + // This thread will sleep until it's time to run the daily task + // Also takes ownership of pool tokio::task::spawn(async { run_daily_task_at_midnight(pool).await; }); @@ -67,13 +70,15 @@ async fn main() { get(graphiql).post_service(GraphQL::new(schema.clone())), ) .layer(cors); + + // TODO: Replace hardcoded address let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, router).await.unwrap(); } async fn run_daily_task_at_midnight(pool: Arc) { loop { - let now = chrono::Local::now(); + let now = chrono::Local::now().with_timezone(&Kolkata); let next_midnight = (now + chrono::Duration::days(1)) .date_naive() .and_hms_opt(0, 0, 0) From 0ba8c00e085a49c78fbfe843ddbb9f031b6c4e92 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Wed, 15 Jan 2025 15:46:46 +0530 Subject: [PATCH 17/38] rewrite midnight attendance records insertion and required models --- src/attendance/daily_task.rs | 337 ++++++++++++++++++----------------- src/models/attendance.rs | 44 ++--- src/models/member.rs | 30 +--- 3 files changed, 196 insertions(+), 215 deletions(-) diff --git a/src/attendance/daily_task.rs b/src/attendance/daily_task.rs index 034c389..e0ece9f 100644 --- a/src/attendance/daily_task.rs +++ b/src/attendance/daily_task.rs @@ -1,189 +1,204 @@ -use chrono::{Datelike, Local, NaiveTime}; +use chrono::{Local, NaiveTime}; use chrono_tz::Asia::Kolkata; use sqlx::PgPool; use std::sync::Arc; +use tracing::{debug, error, info}; -use crate::{ - models::{ - leaderboard::{CodeforcesStats, LeetCodeStats}, - member::Member, - }, - leaderboard::{ - fetch_stats::{fetch_codeforces_stats, fetch_leetcode_stats}, - update_leaderboard::update_leaderboard, - }, -}; -//Scheduled task for moving all members to Attendance table at midnight. +use crate::models::member::Member; + +// TODO: Abstract this down to functions for each task. +// We need to add a record for every member because otherwise Presense will only add present members to the DB, and we will have to JOIN Members and Attendance records for the day to get the absent members. In exchange for increased storage use, we get simpler queries for Home which needs the data for every member for every day so far. But as of Jan 2025, there are less than 50 members in the club and thus shouldn't really create that many rows over time. +/// This function does a number of things, including: +/// * Insert new attendance records everyday for [`presense`](https://www.github.com/amfoss/presense) to update them later in the day. +/// * Fetch stats from both codeforces and leetcode +/// * Use those fetched stats to update the leaderboard +/// * Update attendance streaks for some reason? pub async fn execute_daily_task(pool: Arc) { + add_daily_attendance_records(&*pool).await; + + //fetching the username from tables + // let leetcode_username = sqlx::query_as::<_, LeetCodeStats>( + // "SELECT * FROM leetcode_stats WHERE member_id = $1", + // ) + // .bind(member.id) + // .fetch_optional(pool.as_ref()) + // .await; + + // if let Ok(Some(leetcode_stats)) = leetcode_username { + // let username = leetcode_stats.leetcode_username.clone(); + + // // Fetch and update LeetCode stats + // match fetch_leetcode_stats(pool.clone(), member.id, &username).await { + // Ok(_) => println!("LeetCode stats updated for member ID: {}", member.id), + // Err(e) => eprintln!( + // "Failed to update LeetCode stats for member ID {}: {:?}", + // member.id, e + // ), + // } + // } + + // // Fetch Codeforces username + // let codeforces_username = sqlx::query_as::<_, CodeforcesStats>( + // "SELECT * FROM codeforces_stats WHERE member_id = $1", + // ) + // .bind(member.id) + // .fetch_optional(pool.as_ref()) + // .await; + + // if let Ok(Some(codeforces_stats)) = codeforces_username { + // let username = codeforces_stats.codeforces_handle.clone(); + + // // Fetch and update Codeforces stats + // match fetch_codeforces_stats(pool.clone(), member.id, &username).await { + // Ok(_) => println!("Codeforces stats updated for member ID: {}", member.id), + // Err(e) => eprintln!( + // "Failed to update Codeforces stats for member ID {}: {:?}", + // member.id, e + // ), + // } + // } + + // match update_leaderboard(pool.clone()).await { + // Ok(_) => println!("Leaderboard updated."), + // Err(e) => eprintln!("Failed to update leaderboard: {:?}", e), + // } + + // // Update attendance streak + // update_attendance_streak(member.id, pool.as_ref()).await; +} + +// We need to add a record for every member because otherwise [`Presense`](https://www.github.com/presense) will only add present members to the DB, and we will have to JOIN Members and Attendance records for the day to get the absent members. In exchange for increased storage use, we get simpler queries for Home which needs the data for every member for every day so far. But as of Jan 2025, there are less than 50 members in the club and thus storage really shouldn't be an issue. +/// Inserts new attendance records everyday for [`presense`](https://www.github.com/amfoss/presense) to update them later in the day. +async fn add_daily_attendance_records(pool: &PgPool) { + info!("Adding daily attendance records..."); + let members: Result, sqlx::Error> = sqlx::query_as::<_, Member>("SELECT * FROM Member") - .fetch_all(pool.as_ref()) + .fetch_all(pool) .await; match members { Ok(members) => { - let today = Local::now().with_timezone(&Kolkata); + let today = Local::now().with_timezone(&Kolkata).date_naive(); for member in members { - let timein = NaiveTime::from_hms_opt(0, 0, 0); - let timeout = NaiveTime::from_hms_opt(0, 0, 0); // Default time, can be modified as needed - let attendance = sqlx::query( - "INSERT INTO Attendance (id, date, timein, timeout, is_present) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (id, date) DO NOTHING RETURNING *" + "INSERT INTO Attendance (member_id, date, is_present, time_in, time_out) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (member_id, date) DO NOTHING", ) - .bind(member.id) + .bind(member.member_id) .bind(today) - .bind(timein) - .bind(timeout) - .bind(false) - .execute(pool.as_ref()) + .bind(false) // Default `is_present` is False + .bind(None::) // Default `time_in` is NULL + .bind(None::) // Default `time_out` is NULL + .execute(pool) .await; match attendance { - Ok(_) => println!("Attendance record added for member ID: {}", member.id), - Err(e) => eprintln!( - "Failed to insert attendance for member ID: {}: {:?}", - member.id, e - ), - } - - //fetching the username from tables - let leetcode_username = sqlx::query_as::<_, LeetCodeStats>( - "SELECT * FROM leetcode_stats WHERE member_id = $1", - ) - .bind(member.id) - .fetch_optional(pool.as_ref()) - .await; - - if let Ok(Some(leetcode_stats)) = leetcode_username { - let username = leetcode_stats.leetcode_username.clone(); - - // Fetch and update LeetCode stats - match fetch_leetcode_stats(pool.clone(), member.id, &username).await { - Ok(_) => println!("LeetCode stats updated for member ID: {}", member.id), - Err(e) => eprintln!( - "Failed to update LeetCode stats for member ID {}: {:?}", - member.id, e - ), + Ok(_) => { + debug!( + "Attendance record added for member ID: {}", + member.member_id + ); } - } - - // Fetch Codeforces username - let codeforces_username = sqlx::query_as::<_, CodeforcesStats>( - "SELECT * FROM codeforces_stats WHERE member_id = $1", - ) - .bind(member.id) - .fetch_optional(pool.as_ref()) - .await; - - if let Ok(Some(codeforces_stats)) = codeforces_username { - let username = codeforces_stats.codeforces_handle.clone(); - - // Fetch and update Codeforces stats - match fetch_codeforces_stats(pool.clone(), member.id, &username).await { - Ok(_) => println!("Codeforces stats updated for member ID: {}", member.id), - Err(e) => eprintln!( - "Failed to update Codeforces stats for member ID {}: {:?}", - member.id, e - ), + Err(e) => { + error!( + "Failed to insert attendance for member ID: {}: {:?}", + member.member_id, e + ); } } - - match update_leaderboard(pool.clone()).await { - Ok(_) => println!("Leaderboard updated."), - Err(e) => eprintln!("Failed to update leaderboard: {:?}", e), - } - - // Update attendance streak - update_attendance_streak(member.id, pool.as_ref()).await; } } - Err(e) => eprintln!("Failed to fetch members: {:?}", e), + Err(e) => { + error!("Failed to fetch members: {:?}", e); + } } } // Function to update attendance streak -async fn update_attendance_streak(member_id: i32, pool: &sqlx::PgPool) { - let today = chrono::Local::now() - .with_timezone(&chrono_tz::Asia::Kolkata) - .naive_local(); - let yesterday = today - .checked_sub_signed(chrono::Duration::hours(12)) - .unwrap() - .date(); - - if today.day() == 1 { - let _ = sqlx::query( - r#" - INSERT INTO AttendanceStreak (member_id, month, streak) - VALUES ($1, date_trunc('month', $2::date AT TIME ZONE 'Asia/Kolkata'), 0) - "#, - ) - .bind(member_id) - .bind(today) - .execute(pool) - .await; - println!("Attendance streak created for member ID: {}", member_id); - } - - let present_attendance = sqlx::query_scalar::<_, i64>( - r#" - SELECT COUNT(*) - FROM Attendance - WHERE id = $1 - AND is_present = true - AND date = $2 - "#, - ) - .bind(member_id) - .bind(yesterday) - .fetch_one(pool) - .await; - - match present_attendance { - Ok(1) => { - let existing_streak = sqlx::query_scalar::<_, i32>( - r#" - SELECT streak - FROM AttendanceStreak - WHERE member_id = $1 - AND month = date_trunc('month', $2::date AT TIME ZONE 'Asia/Kolkata') - "#, - ) - .bind(member_id) - .bind(today) - .fetch_optional(pool) - .await; - - match existing_streak { - Ok(Some(streak)) => { - let _ = sqlx::query( - r#" - UPDATE AttendanceStreak - SET streak = $1 - WHERE member_id = $2 - AND month = date_trunc('month', $3::date AT TIME ZONE 'Asia/Kolkata') - "#, - ) - .bind(streak + 1) - .bind(member_id) - .bind(today) - .execute(pool) - .await; - } - Ok(None) => { - println!("No streak found for member ID: {}", member_id); - } - Err(e) => eprintln!("Error checking streak for member ID {}: {:?}", member_id, e), - } - } - Ok(0) => { - println!("Sreak not incremented for member ID: {}", member_id); - } - Ok(_) => eprintln!("Unexpected attendance value for member ID: {}", member_id), - Err(e) => eprintln!( - "Error checking attendance for member ID {}: {:?}", - member_id, e - ), - } -} +// async fn update_attendance_streak(member_id: i32, pool: &sqlx::PgPool) { +// let today = chrono::Local::now() +// .with_timezone(&chrono_tz::Asia::Kolkata) +// .naive_local(); +// let yesterday = today +// .checked_sub_signed(chrono::Duration::hours(12)) +// .unwrap() +// .date(); +// +// if today.day() == 1 { +// let _ = sqlx::query( +// r#" +// INSERT INTO AttendanceStreak (member_id, month, streak) +// VALUES ($1, date_trunc('month', $2::date AT TIME ZONE 'Asia/Kolkata'), 0) +// "#, +// ) +// .bind(member_id) +// .bind(today) +// .execute(pool) +// .await; +// println!("Attendance streak created for member ID: {}", member_id); +// } +// +// let present_attendance = sqlx::query_scalar::<_, i64>( +// r#" +// SELECT COUNT(*) +// FROM Attendance +// WHERE id = $1 +// AND is_present = true +// AND date = $2 +// "#, +// ) +// .bind(member_id) +// .bind(yesterday) +// .fetch_one(pool) +// .await; +// +// match present_attendance { +// Ok(1) => { +// let existing_streak = sqlx::query_scalar::<_, i32>( +// r#" +// SELECT streak +// FROM AttendanceStreak +// WHERE member_id = $1 +// AND month = date_trunc('month', $2::date AT TIME ZONE 'Asia/Kolkata') +// "#, +// ) +// .bind(member_id) +// .bind(today) +// .fetch_optional(pool) +// .await; +// +// match existing_streak { +// Ok(Some(streak)) => { +// let _ = sqlx::query( +// r#" +// UPDATE AttendanceStreak +// SET streak = $1 +// WHERE member_id = $2 +// AND month = date_trunc('month', $3::date AT TIME ZONE 'Asia/Kolkata') +// "#, +// ) +// .bind(streak + 1) +// .bind(member_id) +// .bind(today) +// .execute(pool) +// .await; +// } +// Ok(None) => { +// println!("No streak found for member ID: {}", member_id); +// } +// Err(e) => eprintln!("Error checking streak for member ID {}: {:?}", member_id, e), +// } +// } +// Ok(0) => { +// println!("Sreak not incremented for member ID: {}", member_id); +// } +// Ok(_) => eprintln!("Unexpected attendance value for member ID: {}", member_id), +// Err(e) => eprintln!( +// "Error checking attendance for member ID {}: {:?}", +// member_id, e +// ), +// } +//} diff --git a/src/models/attendance.rs b/src/models/attendance.rs index 4aa7f77..91a181f 100644 --- a/src/models/attendance.rs +++ b/src/models/attendance.rs @@ -1,40 +1,22 @@ use async_graphql::SimpleObject; -use chrono::{NaiveDate, NaiveTime}; use sqlx::FromRow; -//Struct for the Attendance table -#[derive(FromRow, SimpleObject)] +#[derive(FromRow, SimpleObject, Debug)] pub struct Attendance { - pub id: i32, - pub date: NaiveDate, - pub timein: NaiveTime, - pub timeout: NaiveTime, - pub is_present: bool, -} - -#[derive(FromRow, SimpleObject)] -pub struct AttendanceStreak { - pub id: i32, + pub attendance_id: i32, pub member_id: i32, - pub month: NaiveDate, - pub streak: i32, + pub date: chrono::NaiveDate, + pub is_present: bool, + pub time_in: Option, + pub time_out: Option, + pub created_at: chrono::NaiveDateTime, + pub updated_at: chrono::NaiveDateTime, } -#[derive(FromRow, SimpleObject)] +#[derive(FromRow, SimpleObject, Debug)] pub struct AttendanceSummary { - pub max_days: i64, - pub member_attendance: Vec, - pub daily_count: Vec, -} - -#[derive(FromRow, SimpleObject)] -pub struct MemberAttendance { - pub id: i32, - pub present_days: i64, -} - -#[derive(FromRow, SimpleObject)] -pub struct DailyCount { - pub date: NaiveDate, - pub count: i64, + pub member_id: i32, + pub year: i32, + pub month: i32, + pub days_attended: i32, } diff --git a/src/models/member.rs b/src/models/member.rs index ed646a8..03e47eb 100644 --- a/src/models/member.rs +++ b/src/models/member.rs @@ -1,33 +1,17 @@ use async_graphql::SimpleObject; use sqlx::FromRow; -//Struct for the Member table #[derive(FromRow, SimpleObject)] - pub struct Member { - pub id: i32, - pub rollno: String, + pub member_id: i32, + pub roll_no: String, pub name: String, - pub hostel: String, pub email: String, pub sex: String, pub year: i32, - pub macaddress: String, - pub discord_id: Option, - pub group_id: Option, -} - -#[derive(FromRow, SimpleObject)] -pub struct MemberExtended { - pub member: Member, - pub project_title: Option, - pub attendance_count: Option, - pub update_count: Option, -} - -#[derive(FromRow, SimpleObject)] -pub struct StreakUpdate { - pub id: i32, - pub streak: Option, - pub max_streak: Option, + pub hostel: String, + pub mac_address: String, + pub discord_id: String, + pub group_id: i32, + pub created_at: chrono::NaiveDateTime, } From 86ec4e0a21910a68827d63a01f756a7b60b0341b Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Wed, 15 Jan 2025 18:53:02 +0530 Subject: [PATCH 18/38] rewrite attendance record insertion and updation logic This commit unfortunately removes the leaderboard updation via leetcode and codeforces stats to simplify my work for now. Will have to add it back later. --- src/attendance/daily_task.rs | 334 ++++++++++++++++------------------- 1 file changed, 152 insertions(+), 182 deletions(-) diff --git a/src/attendance/daily_task.rs b/src/attendance/daily_task.rs index e0ece9f..564c600 100644 --- a/src/attendance/daily_task.rs +++ b/src/attendance/daily_task.rs @@ -1,204 +1,174 @@ -use chrono::{Local, NaiveTime}; +use chrono::{Datelike, Local, NaiveDate, NaiveTime}; use chrono_tz::Asia::Kolkata; use sqlx::PgPool; use std::sync::Arc; -use tracing::{debug, error, info}; +use tracing::{debug, error, info, trace}; use crate::models::member::Member; -// TODO: Abstract this down to functions for each task. -// We need to add a record for every member because otherwise Presense will only add present members to the DB, and we will have to JOIN Members and Attendance records for the day to get the absent members. In exchange for increased storage use, we get simpler queries for Home which needs the data for every member for every day so far. But as of Jan 2025, there are less than 50 members in the club and thus shouldn't really create that many rows over time. /// This function does a number of things, including: /// * Insert new attendance records everyday for [`presense`](https://www.github.com/amfoss/presense) to update them later in the day. -/// * Fetch stats from both codeforces and leetcode -/// * Use those fetched stats to update the leaderboard -/// * Update attendance streaks for some reason? +/// * Update the AttendanceSummary table pub async fn execute_daily_task(pool: Arc) { - add_daily_attendance_records(&*pool).await; + // Members is queried outside of each function to avoid repetition + let members = sqlx::query_as::<_, Member>("SELECT * FROM Member") + .fetch_all(&*pool) + .await; - //fetching the username from tables - // let leetcode_username = sqlx::query_as::<_, LeetCodeStats>( - // "SELECT * FROM leetcode_stats WHERE member_id = $1", - // ) - // .bind(member.id) - // .fetch_optional(pool.as_ref()) - // .await; - - // if let Ok(Some(leetcode_stats)) = leetcode_username { - // let username = leetcode_stats.leetcode_username.clone(); - - // // Fetch and update LeetCode stats - // match fetch_leetcode_stats(pool.clone(), member.id, &username).await { - // Ok(_) => println!("LeetCode stats updated for member ID: {}", member.id), - // Err(e) => eprintln!( - // "Failed to update LeetCode stats for member ID {}: {:?}", - // member.id, e - // ), - // } - // } - - // // Fetch Codeforces username - // let codeforces_username = sqlx::query_as::<_, CodeforcesStats>( - // "SELECT * FROM codeforces_stats WHERE member_id = $1", - // ) - // .bind(member.id) - // .fetch_optional(pool.as_ref()) - // .await; - - // if let Ok(Some(codeforces_stats)) = codeforces_username { - // let username = codeforces_stats.codeforces_handle.clone(); - - // // Fetch and update Codeforces stats - // match fetch_codeforces_stats(pool.clone(), member.id, &username).await { - // Ok(_) => println!("Codeforces stats updated for member ID: {}", member.id), - // Err(e) => eprintln!( - // "Failed to update Codeforces stats for member ID {}: {:?}", - // member.id, e - // ), - // } - // } - - // match update_leaderboard(pool.clone()).await { - // Ok(_) => println!("Leaderboard updated."), - // Err(e) => eprintln!("Failed to update leaderboard: {:?}", e), - // } - - // // Update attendance streak - // update_attendance_streak(member.id, pool.as_ref()).await; + match members { + Ok(members) => update_attendance(members, &*pool).await, + // TODO: Handle this + Err(e) => (), + }; } // We need to add a record for every member because otherwise [`Presense`](https://www.github.com/presense) will only add present members to the DB, and we will have to JOIN Members and Attendance records for the day to get the absent members. In exchange for increased storage use, we get simpler queries for Home which needs the data for every member for every day so far. But as of Jan 2025, there are less than 50 members in the club and thus storage really shouldn't be an issue. -/// Inserts new attendance records everyday for [`presense`](https://www.github.com/amfoss/presense) to update them later in the day. -async fn add_daily_attendance_records(pool: &PgPool) { - info!("Adding daily attendance records..."); - - let members: Result, sqlx::Error> = - sqlx::query_as::<_, Member>("SELECT * FROM Member") - .fetch_all(pool) - .await; - - match members { - Ok(members) => { - let today = Local::now().with_timezone(&Kolkata).date_naive(); - - for member in members { - let attendance = sqlx::query( - "INSERT INTO Attendance (member_id, date, is_present, time_in, time_out) +/// Inserts new attendance records everyday for [`presense`](https://www.github.com/amfoss/presense) to update them later in the day and updates the AttendanceSummary table to keep track of monthly streaks. +async fn update_attendance(members: Vec, pool: &PgPool) { + info!("Updating attendance..."); + let today = Local::now().with_timezone(&Kolkata).date_naive(); + + for member in members { + let attendance = sqlx::query( + "INSERT INTO Attendance (member_id, date, is_present, time_in, time_out) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (member_id, date) DO NOTHING", - ) - .bind(member.member_id) - .bind(today) - .bind(false) // Default `is_present` is False - .bind(None::) // Default `time_in` is NULL - .bind(None::) // Default `time_out` is NULL - .execute(pool) - .await; - - match attendance { - Ok(_) => { - debug!( - "Attendance record added for member ID: {}", - member.member_id - ); - } - Err(e) => { - error!( - "Failed to insert attendance for member ID: {}: {:?}", - member.member_id, e - ); - } - } + ) + .bind(member.member_id) + .bind(today) + .bind(false) // Default `is_present` is False + .bind(None::) // Default `time_in` is NULL + .bind(None::) // Default `time_out` is NULL + .execute(pool) + .await; + + match attendance { + Ok(_) => { + debug!( + "Attendance record added for member ID: {}", + member.member_id + ); } + Err(e) => { + error!( + "Failed to insert attendance for member ID: {}: {:?}", + member.member_id, e + ); + } + } + update_attendance_summary(member.member_id, pool).await; + } +} + +/// Checks if the member was present yesterday, and if so, increments the `days_attended` value. Otherwise, do +/// nothing. +async fn update_attendance_summary(member_id: i32, pool: &PgPool) { + trace!("Updating summary for member #{}", member_id); + let today = chrono::Local::now().with_timezone(&Kolkata).date_naive(); + let yesterday = today.checked_sub_signed(chrono::Duration::days(1)).unwrap(); // Get yesterday's date + + // Check if the member was present yesterday + let was_present_yesterday = sqlx::query_scalar::<_, bool>( + r#" + SELECT is_present + FROM Attendance + WHERE member_id = $1 AND date = $2 + "#, + ) + .bind(member_id) + .bind(yesterday) + .fetch_one(pool) + .await; + + match was_present_yesterday { + // Member was present yesterday, update the summary + Ok(true) => { + update_days_attended(member_id, today, pool).await; + } + // Member was absent + Ok(false) => { + debug!( + "Member ID: {} was absent yesterday, days_attended remains the same.", + member_id + ); } Err(e) => { - error!("Failed to fetch members: {:?}", e); + error!("Could not fetch records from DB. Error: {}", e); } } } -// Function to update attendance streak -// async fn update_attendance_streak(member_id: i32, pool: &sqlx::PgPool) { -// let today = chrono::Local::now() -// .with_timezone(&chrono_tz::Asia::Kolkata) -// .naive_local(); -// let yesterday = today -// .checked_sub_signed(chrono::Duration::hours(12)) -// .unwrap() -// .date(); -// -// if today.day() == 1 { -// let _ = sqlx::query( -// r#" -// INSERT INTO AttendanceStreak (member_id, month, streak) -// VALUES ($1, date_trunc('month', $2::date AT TIME ZONE 'Asia/Kolkata'), 0) -// "#, -// ) -// .bind(member_id) -// .bind(today) -// .execute(pool) -// .await; -// println!("Attendance streak created for member ID: {}", member_id); -// } -// -// let present_attendance = sqlx::query_scalar::<_, i64>( -// r#" -// SELECT COUNT(*) -// FROM Attendance -// WHERE id = $1 -// AND is_present = true -// AND date = $2 -// "#, -// ) -// .bind(member_id) -// .bind(yesterday) -// .fetch_one(pool) -// .await; -// -// match present_attendance { -// Ok(1) => { -// let existing_streak = sqlx::query_scalar::<_, i32>( -// r#" -// SELECT streak -// FROM AttendanceStreak -// WHERE member_id = $1 -// AND month = date_trunc('month', $2::date AT TIME ZONE 'Asia/Kolkata') -// "#, -// ) -// .bind(member_id) -// .bind(today) -// .fetch_optional(pool) -// .await; -// -// match existing_streak { -// Ok(Some(streak)) => { -// let _ = sqlx::query( -// r#" -// UPDATE AttendanceStreak -// SET streak = $1 -// WHERE member_id = $2 -// AND month = date_trunc('month', $3::date AT TIME ZONE 'Asia/Kolkata') -// "#, -// ) -// .bind(streak + 1) -// .bind(member_id) -// .bind(today) -// .execute(pool) -// .await; -// } -// Ok(None) => { -// println!("No streak found for member ID: {}", member_id); -// } -// Err(e) => eprintln!("Error checking streak for member ID {}: {:?}", member_id, e), -// } -// } -// Ok(0) => { -// println!("Sreak not incremented for member ID: {}", member_id); -// } -// Ok(_) => eprintln!("Unexpected attendance value for member ID: {}", member_id), -// Err(e) => eprintln!( -// "Error checking attendance for member ID {}: {:?}", -// member_id, e -// ), -// } -//} +/// Increments the `days_attended` value for the given member in the given month. +async fn update_days_attended(member_id: i32, today: NaiveDate, pool: &PgPool) { + // Convert year and month into i32 cause SQLx cannot encode u32 into database types + let month: i32 = (today.month0() + 1) as i32; + let year: i32 = today.year_ce().1 as i32; + + // Check if there’s an existing summary for the current month + let existing_days_attended = sqlx::query_scalar::<_, i32>( + r#" + SELECT days_attended + FROM AttendanceSummary + WHERE member_id = $1 + AND year = $2 + AND month = $3 + "#, + ) + .bind(member_id) + .bind(year) + .bind(month) + .fetch_optional(pool) + .await; + + match existing_days_attended { + Ok(Some(days_attended)) => { + sqlx::query( + r#" + UPDATE AttendanceSummary + SET days_attended = days_attended + 1 + WHERE member_id = $2 + AND year = $3 + AND month = $4 + "#, + ) + .bind(member_id) + .bind(year) + .bind(month) + .execute(pool) + .await + .unwrap(); + + debug!( + "Updated days_attended for member ID: {}. New days_attended: {}", + member_id, + days_attended + 1 + ); + } + // No summary exists for this month, create a new one + Ok(None) => { + sqlx::query( + r#" + INSERT INTO AttendanceSummary (member_id, year, month, days_attended) + VALUES ($1, $2, 1) + "#, + ) + .bind(member_id) + .bind(year) + .bind(month) + .execute(pool) + .await + .unwrap(); + + debug!( + "Created new streak for member ID: {} for the month.", + member_id + ); + } + Err(e) => { + error!( + "Error checking or updating streak for member ID {}: {:?}", + member_id, e + ); + } + } +} From 7a9a6ee861040c1f382f64de29e6381d1dd8afd3 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Wed, 15 Jan 2025 19:12:10 +0530 Subject: [PATCH 19/38] remove leaderboard and active projects feature --- src/graphql/mutations.rs | 104 --------------- src/graphql/query.rs | 79 +---------- src/leaderboard/fetch_stats.rs | 180 -------------------------- src/leaderboard/mod.rs | 2 - src/leaderboard/update_leaderboard.rs | 141 -------------------- src/lib.rs | 1 - src/models/leaderboard.rs | 73 ----------- src/models/mod.rs | 2 - src/models/projects.rs | 9 -- 9 files changed, 1 insertion(+), 590 deletions(-) delete mode 100644 src/leaderboard/fetch_stats.rs delete mode 100644 src/leaderboard/mod.rs delete mode 100644 src/leaderboard/update_leaderboard.rs delete mode 100644 src/models/leaderboard.rs delete mode 100644 src/models/projects.rs diff --git a/src/graphql/mutations.rs b/src/graphql/mutations.rs index beab8a3..c7b4eb4 100644 --- a/src/graphql/mutations.rs +++ b/src/graphql/mutations.rs @@ -14,7 +14,6 @@ use root::models::{ attendance::Attendance, leaderboard::{CodeforcesStats, LeetCodeStats}, member::Member, - member::StreakUpdate, projects::ActiveProjects, }; @@ -205,61 +204,6 @@ impl MutationRoot { Ok(attendance) } - //here when user changes the handle, it just updates the handle in the database without updating the other values till midnight - - async fn add_or_update_leetcode_username( - &self, - ctx: &Context<'_>, - member_id: i32, - username: String, - ) -> Result { - let pool = ctx - .data::>() - .expect("Pool not found in context"); - - let result = sqlx::query_as::<_, LeetCodeStats>( - " - INSERT INTO leetcode_stats (member_id, leetcode_username, problems_solved, easy_solved, medium_solved, hard_solved, contests_participated, best_rank, total_contests) - VALUES ($1, $2, 0, 0, 0, 0, 0, 0, 0) - ON CONFLICT (member_id) DO UPDATE - SET leetcode_username = $2 - RETURNING * - " - ) - .bind(member_id) - .bind(username) - .fetch_one(pool.as_ref()) - .await?; - - Ok(result) - } - - async fn add_or_update_codeforces_handle( - &self, - ctx: &Context<'_>, - member_id: i32, - handle: String, - ) -> Result { - let pool = ctx - .data::>() - .expect("Pool not found in context"); - - let result = sqlx::query_as::<_, CodeforcesStats>( - " - INSERT INTO codeforces_stats (member_id, codeforces_handle, codeforces_rating, max_rating, contests_participated) - VALUES ($1, $2, 0, 0, 0) - ON CONFLICT (member_id) DO UPDATE - SET codeforces_handle = $2 - RETURNING * - " - ) - .bind(member_id) - .bind(handle) - .fetch_one(pool.as_ref()) - .await?; - - Ok(result) - } async fn update_streak( &self, ctx: &Context<'_>, @@ -326,52 +270,4 @@ impl MutationRoot { } } } - - async fn set_active_project( - &self, - ctx: &Context<'_>, - id: i32, - project_name: String, - ) -> Result { - let pool = ctx - .data::>() - .expect("Pool not found in context"); - - let active_project = sqlx::query_as::<_, ActiveProjects>( - " - INSERT INTO ActiveProjects (member_id,project_title) - VALUES ($1,$2) - RETURNING * - ", - ) - .bind(id) - .bind(project_name) - .fetch_one(pool.as_ref()) - .await?; - - Ok(active_project) - } - - async fn remove_active_project( - &self, - ctx: &Context<'_>, - project_id: i32, - ) -> Result { - let pool = ctx - .data::>() - .expect("Pool not found in context"); - - let active_project = sqlx::query_as::<_, ActiveProjects>( - " - DELETE FROM ActiveProjects - WHERE id = $1 - RETURNING * - ", - ) - .bind(project_id) - .fetch_one(pool.as_ref()) - .await?; - - Ok(active_project) - } } diff --git a/src/graphql/query.rs b/src/graphql/query.rs index 6464315..98024c1 100644 --- a/src/graphql/query.rs +++ b/src/graphql/query.rs @@ -2,12 +2,7 @@ use async_graphql::{Context, Object}; use chrono::NaiveDate; use root::models::{ attendance::Attendance, - leaderboard::{CodeforcesStatsWithName, LeaderboardWithMember, LeetCodeStatsWithName}, - member::{Member, StreakUpdate}, -}; -use root::models::{ - attendance::{AttendanceStreak, AttendanceSummary, DailyCount, MemberAttendance}, - projects::ActiveProjects, + member::Member, }; use sqlx::PgPool; use std::sync::Arc; @@ -16,7 +11,6 @@ pub struct QueryRoot; #[Object] impl QueryRoot { - //Query for retrieving the members async fn get_member(&self, ctx: &Context<'_>) -> Result, sqlx::Error> { let pool = ctx .data::>() @@ -27,62 +21,6 @@ impl QueryRoot { Ok(users) } - async fn get_unified_leaderboard( - &self, - ctx: &Context<'_>, - ) -> Result, sqlx::Error> { - let pool = ctx - .data::>() - .expect("Pool not found in context"); - let leaderboard = sqlx::query_as::<_, LeaderboardWithMember>( - "SELECT l.*, m.name AS member_name - FROM leaderboard l - JOIN member m ON l.member_id = m.id - ORDER BY unified_score DESC", - ) - .fetch_all(pool.as_ref()) - .await?; - Ok(leaderboard) - } - - async fn get_leetcode_stats( - &self, - ctx: &Context<'_>, - ) -> Result, sqlx::Error> { - let pool = ctx - .data::>() - .expect("Pool not found in context"); - let leetcode_stats = sqlx::query_as::<_, LeetCodeStatsWithName>( - "SELECT l.*, m.name AS member_name - FROM leetcode_stats l - JOIN member m ON l.member_id = m.id - ORDER BY best_rank - ", - ) - .fetch_all(pool.as_ref()) - .await?; - Ok(leetcode_stats) - } - - async fn get_codeforces_stats( - &self, - ctx: &Context<'_>, - ) -> Result, sqlx::Error> { - // let pool = ctx.data::()?; - let pool = ctx - .data::>() - .expect("Pool not found in context"); - let codeforces_stats = sqlx::query_as::<_, CodeforcesStatsWithName>( - "SELECT c.*, m.name AS member_name - FROM codeforces_stats c - JOIN member m ON c.member_id = m.id - ORDER BY max_rating DESC", - ) - .fetch_all(pool.as_ref()) - .await?; - Ok(codeforces_stats) - } - //Query for retrieving the attendance based on date async fn get_attendance( &self, @@ -227,19 +165,4 @@ impl QueryRoot { Ok(dates) } - - pub async fn get_projects( - &self, - ctx: &Context<'_>, - ) -> Result, sqlx::Error> { - let pool = ctx - .data::>() - .expect("Pool not found in context"); - - let active_projects = sqlx::query_as::<_, ActiveProjects>("SELECT * FROM ActiveProjects") - .fetch_all(pool.as_ref()) - .await?; - - Ok(active_projects) - } } diff --git a/src/leaderboard/fetch_stats.rs b/src/leaderboard/fetch_stats.rs deleted file mode 100644 index 824286e..0000000 --- a/src/leaderboard/fetch_stats.rs +++ /dev/null @@ -1,180 +0,0 @@ -use serde_json::Value; -use sqlx::PgPool; -use std::sync::Arc; - -pub async fn fetch_leetcode_stats( - pool: Arc, - member_id: i32, - username: &str, -) -> Result<(), Box> { - let client = reqwest::Client::new(); - let url = "https://leetcode.com/graphql"; - let query = r#" - query userProfile($username: String!) { - userContestRanking(username: $username) { - - attendedContestsCount - - } - matchedUser(username: $username) { - profile { - ranking - } - submitStats { - acSubmissionNum { - difficulty - count - } - } - contestBadge { - name - } - - } - } - "#; - - let response = client - .post(url) - .header("Content-Type", "application/json") - .json(&serde_json::json!({ - "query": query, - "variables": { "username": username } - })) - .send() - .await?; - - let data: Value = response.json().await?; - - let submissions = &data["data"]["matchedUser"]["submitStats"]["acSubmissionNum"]; - let mut problems_solved = 0; - let mut easy_solved = 0; - let mut medium_solved = 0; - let mut hard_solved = 0; - - if let Some(stats) = submissions.as_array() { - for stat in stats { - let count = stat["count"].as_i64().unwrap_or(0) as i32; - match stat["difficulty"].as_str().unwrap_or("") { - "Easy" => easy_solved = count, - "Medium" => medium_solved = count, - "Hard" => hard_solved = count, - "All" => problems_solved = count, - _ => {} - } - } - } - - let user_contest_info = &data["data"]["userContestRanking"]; - let contests_participated = user_contest_info["attendedContestsCount"] - .as_i64() - .unwrap_or(0) as i32; - // let best_rank = contests["globalRanking"].as_i64().unwrap_or(0) as i32; //best rank is not needed, might need later for user profile - let total_contests = contests_participated; // Since total contests are equal to attended contests for simplicity - - // Extract ranking - let rank = data["data"]["matchedUser"]["profile"]["ranking"] - .as_i64() - .map(|v| v as i32) - .unwrap_or(0); - - let update_result = sqlx::query( - " - INSERT INTO leetcode_stats ( - member_id, leetcode_username, problems_solved, easy_solved, medium_solved, - hard_solved, contests_participated, best_rank, total_contests - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) - ON CONFLICT (member_id) DO UPDATE SET - leetcode_username = EXCLUDED.leetcode_username, - problems_solved = EXCLUDED.problems_solved, - easy_solved = EXCLUDED.easy_solved, - medium_solved = EXCLUDED.medium_solved, - hard_solved = EXCLUDED.hard_solved, - contests_participated = EXCLUDED.contests_participated, - best_rank = EXCLUDED.best_rank, - total_contests = EXCLUDED.total_contests - ", - ) - .bind(member_id) - .bind(username) - .bind(problems_solved) - .bind(easy_solved) - .bind(medium_solved) - .bind(hard_solved) - .bind(contests_participated) - .bind(rank) - .bind(total_contests) - .execute(pool.as_ref()) - .await; - - match update_result { - Ok(_) => println!("LeetCode stats updated for member ID: {}", member_id), - Err(e) => eprintln!( - "Failed to update LeetCode stats for member ID {}: {:?}", - member_id, e - ), - } - - Ok(()) -} - -pub async fn fetch_codeforces_stats( - pool: Arc, - member_id: i32, - username: &str, -) -> Result<(), Box> { - let url = format!("https://codeforces.com/api/user.rating?handle={}", username); - let response = reqwest::get(&url).await?.text().await?; - let data: Value = serde_json::from_str(&response)?; - - if data["status"] == "OK" { - if let Some(results) = data["result"].as_array() { - let contests_participated = results.len() as i32; - - // Calculate the user's current and max ratings - let mut max_rating = 0; - let mut codeforces_rating = 0; - - for contest in results { - if let Some(new_rating) = contest["newRating"].as_i64() { - codeforces_rating = new_rating as i32; - max_rating = max_rating.max(codeforces_rating); - } - } - - let update_result = sqlx::query( - r#" - INSERT INTO codeforces_stats ( - member_id, codeforces_handle, codeforces_rating, max_rating, contests_participated - ) - VALUES ($1, $2, $3, $4, $5) - ON CONFLICT (member_id) DO UPDATE SET - codeforces_handle = EXCLUDED.codeforces_handle, - codeforces_rating = EXCLUDED.codeforces_rating, - max_rating = EXCLUDED.max_rating, - contests_participated = EXCLUDED.contests_participated - "#, - ) - .bind(member_id) - .bind(username) - .bind(codeforces_rating) - .bind(max_rating) - .bind(contests_participated) - .execute(pool.as_ref()) - .await; - - match update_result { - Ok(_) => println!("Codeforces stats updated for member ID: {}", member_id), - Err(e) => eprintln!( - "Failed to update Codeforces stats for member ID {}: {:?}", - member_id, e - ), - } - - return Ok(()); - } - } - - Err(format!("Failed to fetch stats for Codeforces handle: {}", username).into()) -} diff --git a/src/leaderboard/mod.rs b/src/leaderboard/mod.rs deleted file mode 100644 index 5dbae36..0000000 --- a/src/leaderboard/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod fetch_stats; -pub mod update_leaderboard; diff --git a/src/leaderboard/update_leaderboard.rs b/src/leaderboard/update_leaderboard.rs deleted file mode 100644 index f575957..0000000 --- a/src/leaderboard/update_leaderboard.rs +++ /dev/null @@ -1,141 +0,0 @@ -use sqlx::PgPool; -use std::sync::Arc; - -pub async fn update_leaderboard(pool: Arc) -> Result<(), Box> { - let leetcode_stats: Result, _> = - sqlx::query_as::<_, (i32, i32, i32, i32, i32, i32, i32, i32)>( - "SELECT id, member_id, problems_solved, easy_solved, medium_solved, hard_solved, contests_participated, best_rank - FROM leetcode_stats", - ) - .fetch_all(pool.as_ref()) - .await; - - let leetcode_stats = match leetcode_stats { - Ok(stats) => stats, - Err(e) => { - eprintln!("Failed to fetch LeetCode stats: {:?}", e); - return Err(Box::new(e)); - } - }; - - let codeforces_stats: Result, _> = - sqlx::query_as::<_, (i32, i32, i32, i32, i32)>( - "SELECT id, member_id, codeforces_rating, max_rating, contests_participated - FROM codeforces_stats", - ) - .fetch_all(pool.as_ref()) - .await; - - let codeforces_stats = match codeforces_stats { - Ok(stats) => stats, - Err(e) => { - eprintln!("Failed to fetch Codeforces stats: {:?}", e); - return Err(Box::new(e)); - } - }; - - // Create a lookup table for Codeforces stats - let cf_lookup: std::collections::HashMap = codeforces_stats - .iter() - .map( - |(_, member_id, codeforces_rating, max_rating, contests_participated)| { - ( - *member_id, - (*codeforces_rating, *max_rating, *contests_participated), - ) - }, - ) - .collect(); - - for ( - _, - member_id, - _, - easy_solved, - medium_solved, - hard_solved, - contests_participated, - best_rank, - ) in &leetcode_stats - { - //todo: the algorith is not correct, might have to find a good one - let leetcode_score = (5 * easy_solved) - + (10 * medium_solved) - + (20 * hard_solved) - + (2 * contests_participated) - + (100 - best_rank / 10).max(0); - - let codeforces_score = cf_lookup - .get(member_id) - .map_or(0, |(rating, max_rating, contests)| { - (rating / 10) + (max_rating / 20) + (5 * contests) - }); - - let unified_score = leetcode_score + codeforces_score; - - let result = sqlx::query( - "INSERT INTO leaderboard (member_id, leetcode_score, codeforces_score, unified_score, last_updated) - VALUES ($1, $2, $3, $4, NOW()) - ON CONFLICT (member_id) DO UPDATE SET - leetcode_score = EXCLUDED.leetcode_score, - codeforces_score = EXCLUDED.codeforces_score, - unified_score = EXCLUDED.unified_score, - last_updated = NOW()", - ) - .bind(member_id) - .bind(leetcode_score) - .bind(codeforces_score) - .bind(unified_score) - .execute(pool.as_ref()) - .await; - - if let Err(e) = result { - eprintln!( - "Failed to update leaderboard for member ID: {}: {:?}", - member_id, e - ); - } - } - - // Process remaining Codeforces-only members - for (_, member_id, codeforces_rating, max_rating, contests_participated) in &codeforces_stats { - if leetcode_stats - .iter() - .any(|(_, lc_member_id, _, _, _, _, _, _)| lc_member_id == member_id) - { - continue; - } - - let codeforces_score = - (codeforces_rating / 10) + (max_rating / 20) + (5 * contests_participated); - - // Default LeetCode score is 0 - let leetcode_score = 0; - let unified_score = codeforces_score; - - let result = sqlx::query( - "INSERT INTO leaderboard (member_id, leetcode_score, codeforces_score, unified_score, last_updated) - VALUES ($1, $2, $3, $4, NOW()) - ON CONFLICT (member_id) DO UPDATE SET - leetcode_score = EXCLUDED.leetcode_score, - codeforces_score = EXCLUDED.codeforces_score, - unified_score = EXCLUDED.unified_score, - last_updated = NOW()", - ) - .bind(member_id) - .bind(leetcode_score) - .bind(codeforces_score) - .bind(unified_score) - .execute(pool.as_ref()) - .await; - - if let Err(e) = result { - eprintln!( - "Failed to update leaderboard for member ID: {}: {:?}", - member_id, e - ); - } - } - - Ok(()) -} diff --git a/src/lib.rs b/src/lib.rs index b49bb8c..19aefd8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,3 @@ // Need this to expose models module for tests pub mod attendance; -pub mod leaderboard; pub mod models; diff --git a/src/models/leaderboard.rs b/src/models/leaderboard.rs deleted file mode 100644 index aa74f5e..0000000 --- a/src/models/leaderboard.rs +++ /dev/null @@ -1,73 +0,0 @@ -use async_graphql::SimpleObject; -use sqlx::FromRow; - -#[derive(FromRow, SimpleObject)] -pub struct Leaderboard { - pub id: i32, - pub member_id: i32, - pub leetcode_score: Option, - pub codeforces_score: Option, - pub unified_score: i32, - pub last_updated: Option, -} - -#[derive(FromRow, SimpleObject)] -pub struct LeaderboardWithMember { - pub id: i32, - pub member_id: i32, - pub member_name: String, - pub leetcode_score: Option, - pub codeforces_score: Option, - pub unified_score: i32, - pub last_updated: Option, -} - -#[derive(FromRow, SimpleObject)] -pub struct LeetCodeStats { - pub id: i32, - pub member_id: i32, - pub leetcode_username: String, - pub problems_solved: i32, - pub easy_solved: i32, - pub medium_solved: i32, - pub hard_solved: i32, - pub contests_participated: i32, - pub best_rank: i32, - pub total_contests: i32, -} - -#[derive(FromRow, SimpleObject)] -pub struct LeetCodeStatsWithName { - pub id: i32, - pub member_id: i32, - pub member_name: String, - pub leetcode_username: String, - pub problems_solved: i32, - pub easy_solved: i32, - pub medium_solved: i32, - pub hard_solved: i32, - pub contests_participated: i32, - pub best_rank: i32, - pub total_contests: i32, -} - -#[derive(FromRow, SimpleObject)] -pub struct CodeforcesStats { - pub id: i32, - pub member_id: i32, - pub codeforces_handle: String, - pub codeforces_rating: i32, - pub max_rating: i32, - pub contests_participated: i32, -} - -#[derive(FromRow, SimpleObject)] -pub struct CodeforcesStatsWithName { - pub id: i32, - pub member_id: i32, - pub member_name: String, - pub codeforces_handle: String, - pub codeforces_rating: i32, - pub max_rating: i32, - pub contests_participated: i32, -} diff --git a/src/models/mod.rs b/src/models/mod.rs index 11cf39a..026cf0a 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,4 +1,2 @@ pub mod attendance; -pub mod leaderboard; pub mod member; -pub mod projects; diff --git a/src/models/projects.rs b/src/models/projects.rs deleted file mode 100644 index 448c5e5..0000000 --- a/src/models/projects.rs +++ /dev/null @@ -1,9 +0,0 @@ -use async_graphql::SimpleObject; -use sqlx::FromRow; - -#[derive(FromRow, SimpleObject)] -pub struct ActiveProjects { - id: i32, - member_id: i32, - project_title: Option, -} From 867ac2d03936cea68871a67763a0578ddc30b4b2 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Wed, 15 Jan 2025 19:56:18 +0530 Subject: [PATCH 20/38] documentation changes and additions --- src/attendance/daily_task.rs | 9 ++++++--- src/main.rs | 5 +++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/attendance/daily_task.rs b/src/attendance/daily_task.rs index 564c600..bfead6c 100644 --- a/src/attendance/daily_task.rs +++ b/src/attendance/daily_task.rs @@ -16,9 +16,10 @@ pub async fn execute_daily_task(pool: Arc) { .await; match members { + // Add additional daily tasks such as leaderboard updates to the Ok(members) arm Ok(members) => update_attendance(members, &*pool).await, // TODO: Handle this - Err(e) => (), + Err(_) => (), }; } @@ -29,6 +30,7 @@ async fn update_attendance(members: Vec, pool: &PgPool) { let today = Local::now().with_timezone(&Kolkata).date_naive(); for member in members { + // Insert blank rows for each member let attendance = sqlx::query( "INSERT INTO Attendance (member_id, date, is_present, time_in, time_out) VALUES ($1, $2, $3, $4, $5) @@ -56,12 +58,13 @@ async fn update_attendance(members: Vec, pool: &PgPool) { ); } } + // This could have been called in `execute_daily_task()` but that would require us to loop through members twice. + // Whether or not inserting attendance failed, Root will attempt to update AttendanceSummary. This can potentially fail too since insertion failed earlier. However, these two do not depend on each other and one of them failing is no reason to avoid trying the other. update_attendance_summary(member.member_id, pool).await; } } -/// Checks if the member was present yesterday, and if so, increments the `days_attended` value. Otherwise, do -/// nothing. +/// Checks if the member was present yesterday, and if so, increments the `days_attended` value. Otherwise, do nothing. async fn update_attendance_summary(member_id: i32, pool: &PgPool) { trace!("Updating summary for member #{}", member_id); let today = chrono::Local::now().with_timezone(&Kolkata).date_naive(); diff --git a/src/main.rs b/src/main.rs index 23b26ef..c0cdaa4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -use crate::routes::graphiql; use async_graphql::EmptySubscription; use async_graphql_axum::GraphQL; use axum::{ @@ -64,10 +63,11 @@ async fn main() { .allow_headers(tower_http::cors::Any); info!("Starting Root..."); + // TODO: Avoid exposing the GraphiQL interface in prod. let router = Router::new() .route( "/", - get(graphiql).post_service(GraphQL::new(schema.clone())), + get(routes::graphiql).post_service(GraphQL::new(schema.clone())), ) .layer(cors); @@ -76,6 +76,7 @@ async fn main() { axum::serve(listener, router).await.unwrap(); } +/// Sleep till midnight, then run the 'execute_daily_task' function. async fn run_daily_task_at_midnight(pool: Arc) { loop { let now = chrono::Local::now().with_timezone(&Kolkata); From 7762c85070319834c858ae97723035e8384a04c4 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Fri, 17 Jan 2025 11:52:21 +0530 Subject: [PATCH 21/38] ignore .log files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b01dd90..c12db7a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ Secrets*.toml backups/ .env +*.log From 3518e184c074e8e7e93f6f5ba122e1befff86785 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Fri, 17 Jan 2025 12:41:12 +0530 Subject: [PATCH 22/38] rewrite graphql interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit changes a lot more things than just the graphql interface. Also likely introduces a few extra regressions. ¯\_(ツ)_/¯ --- Cargo.lock | 34 ++- Cargo.toml | 2 +- migrations/20250114180047_create_tables.sql | 4 +- src/{attendance => daily_task}/daily_task.rs | 6 +- src/{attendance => daily_task}/mod.rs | 0 src/graphql/mod.rs | 2 +- src/graphql/mutations.rs | 273 ------------------ src/graphql/mutations/attendance_mutations.rs | 41 +++ src/graphql/mutations/member_mutations.rs | 39 +++ src/graphql/mutations/mod.rs | 3 + src/graphql/mutations/streak_mutations.rs | 49 ++++ src/graphql/queries/attendance_queries.rs | 69 +++++ src/graphql/queries/member_queries.rs | 128 ++++++++ src/graphql/queries/mod.rs | 3 + src/graphql/queries/streak_queries.rs | 70 +++++ src/graphql/query.rs | 168 ----------- src/lib.rs | 3 - src/main.rs | 74 ++++- src/models/attendance.rs | 44 ++- src/models/member.rs | 30 +- src/models/mod.rs | 1 + src/models/status_update_streak.rs | 21 ++ src/routes.rs | 2 + 23 files changed, 590 insertions(+), 476 deletions(-) rename src/{attendance => daily_task}/daily_task.rs (97%) rename src/{attendance => daily_task}/mod.rs (100%) delete mode 100644 src/graphql/mutations.rs create mode 100644 src/graphql/mutations/attendance_mutations.rs create mode 100644 src/graphql/mutations/member_mutations.rs create mode 100644 src/graphql/mutations/mod.rs create mode 100644 src/graphql/mutations/streak_mutations.rs create mode 100644 src/graphql/queries/attendance_queries.rs create mode 100644 src/graphql/queries/member_queries.rs create mode 100644 src/graphql/queries/mod.rs create mode 100644 src/graphql/queries/streak_queries.rs delete mode 100644 src/graphql/query.rs delete mode 100644 src/lib.rs create mode 100644 src/models/status_update_streak.rs diff --git a/Cargo.lock b/Cargo.lock index 398cd84..d746a38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1360,6 +1360,15 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "matchit" version = "0.7.3" @@ -1854,8 +1863,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -1866,9 +1884,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.5" @@ -2822,10 +2846,14 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] diff --git a/Cargo.toml b/Cargo.toml index 57f13f3..12a6e0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,5 +21,5 @@ serde_json = "1.0" reqwest = { version = "0.11.27", features = ["json"] } config = "0.13" tracing = "0.1.41" -tracing-subscriber = "0.3.19" +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } dotenv = "0.15.0" diff --git a/migrations/20250114180047_create_tables.sql b/migrations/20250114180047_create_tables.sql index e91dcc6..5d168f7 100644 --- a/migrations/20250114180047_create_tables.sql +++ b/migrations/20250114180047_create_tables.sql @@ -32,7 +32,7 @@ CREATE TABLE Attendance ( (is_present = FALSE AND time_in IS NULL AND time_out IS NULL) ), CHECK (is_present = FALSE OR date <= CURRENT_DATE), - CHECK (time_out IS NULL OR time_out > time_in), + CHECK (time_out IS NULL OR time_out >= time_in), UNIQUE (member_id, date) ); @@ -58,7 +58,7 @@ CREATE TABLE AttendanceSummary ( primary key (member_id, year, month) ); -CREATE TABLE StatusUpdateStreaks ( +CREATE TABLE StatusUpdateStreak ( member_id INT REFERENCES Member(member_id) ON DELETE CASCADE, current_streak int NOT NULL DEFAULT 0, max_streak INT NOT NULL, diff --git a/src/attendance/daily_task.rs b/src/daily_task/daily_task.rs similarity index 97% rename from src/attendance/daily_task.rs rename to src/daily_task/daily_task.rs index bfead6c..a49181b 100644 --- a/src/attendance/daily_task.rs +++ b/src/daily_task/daily_task.rs @@ -2,7 +2,7 @@ use chrono::{Datelike, Local, NaiveDate, NaiveTime}; use chrono_tz::Asia::Kolkata; use sqlx::PgPool; use std::sync::Arc; -use tracing::{debug, error, info, trace}; +use tracing::{debug, error}; use crate::models::member::Member; @@ -26,7 +26,7 @@ pub async fn execute_daily_task(pool: Arc) { // We need to add a record for every member because otherwise [`Presense`](https://www.github.com/presense) will only add present members to the DB, and we will have to JOIN Members and Attendance records for the day to get the absent members. In exchange for increased storage use, we get simpler queries for Home which needs the data for every member for every day so far. But as of Jan 2025, there are less than 50 members in the club and thus storage really shouldn't be an issue. /// Inserts new attendance records everyday for [`presense`](https://www.github.com/amfoss/presense) to update them later in the day and updates the AttendanceSummary table to keep track of monthly streaks. async fn update_attendance(members: Vec, pool: &PgPool) { - info!("Updating attendance..."); + debug!("Updating attendance..."); let today = Local::now().with_timezone(&Kolkata).date_naive(); for member in members { @@ -66,7 +66,7 @@ async fn update_attendance(members: Vec, pool: &PgPool) { /// Checks if the member was present yesterday, and if so, increments the `days_attended` value. Otherwise, do nothing. async fn update_attendance_summary(member_id: i32, pool: &PgPool) { - trace!("Updating summary for member #{}", member_id); + debug!("Updating summary for member #{}", member_id); let today = chrono::Local::now().with_timezone(&Kolkata).date_naive(); let yesterday = today.checked_sub_signed(chrono::Duration::days(1)).unwrap(); // Get yesterday's date diff --git a/src/attendance/mod.rs b/src/daily_task/mod.rs similarity index 100% rename from src/attendance/mod.rs rename to src/daily_task/mod.rs diff --git a/src/graphql/mod.rs b/src/graphql/mod.rs index 47c3b88..c2baadf 100644 --- a/src/graphql/mod.rs +++ b/src/graphql/mod.rs @@ -1,2 +1,2 @@ pub mod mutations; -pub mod query; +pub mod queries; diff --git a/src/graphql/mutations.rs b/src/graphql/mutations.rs deleted file mode 100644 index c7b4eb4..0000000 --- a/src/graphql/mutations.rs +++ /dev/null @@ -1,273 +0,0 @@ -use ::chrono::Local; -use async_graphql::{Context, Object}; -use chrono::{NaiveDate, NaiveTime}; -use chrono_tz::Asia::Kolkata; -use hmac::{Hmac, Mac}; -use sha2::Sha256; -use sqlx::types::chrono; -use sqlx::PgPool; -use std::sync::Arc; - -type HmacSha256 = Hmac; - -use root::models::{ - attendance::Attendance, - leaderboard::{CodeforcesStats, LeetCodeStats}, - member::Member, - projects::ActiveProjects, -}; - -pub struct MutationRoot; - -#[Object] -impl MutationRoot { - //Mutation for adding members to the Member table - async fn add_member( - &self, - ctx: &Context<'_>, - rollno: String, - name: String, - hostel: String, - email: String, - sex: String, - year: i32, - macaddress: String, - discord_id: String, - group_id: i32, - ) -> Result { - let pool = ctx - .data::>() - .expect("Pool not found in context"); - - let member = sqlx::query_as::<_, Member>( - "INSERT INTO Member (rollno, name, hostel, email, sex, year, macaddress, discord_id, group_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *" - ) - .bind(rollno) - .bind(name) - .bind(hostel) - .bind(email) - .bind(sex) - .bind(year) - .bind(macaddress) - .bind(discord_id) - .bind(group_id) - .fetch_one(pool.as_ref()) - .await?; - - Ok(member) - } - - async fn edit_member( - &self, - ctx: &Context<'_>, - id: i32, - hostel: String, - year: i32, - macaddress: String, - discord_id: String, - group_id: i32, - hmac_signature: String, - ) -> Result { - let pool = ctx - .data::>() - .expect("Pool not found in context"); - - let secret_key = ctx - .data::() - .expect("HMAC secret not found in context"); - - let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes()) - .expect("HMAC can take key of any size"); - - let message = format!( - "{}{}{}{}{}{}", - id, hostel, year, macaddress, discord_id, group_id - ); - mac.update(message.as_bytes()); - - let expected_signature = mac.finalize().into_bytes(); - - // Convert the received HMAC signature from the client to bytes for comparison - let received_signature = hex::decode(hmac_signature) - .map_err(|_| sqlx::Error::Protocol("Invalid HMAC signature".into()))?; - - if expected_signature.as_slice() != received_signature.as_slice() { - return Err(sqlx::Error::Protocol("HMAC verification failed".into())); - } - - let member = sqlx::query_as::<_, Member>( - " - UPDATE Member - SET - hostel = CASE WHEN $1 = '' THEN hostel ELSE $1 END, - year = CASE WHEN $2 = 0 THEN year ELSE $2 END, - macaddress = CASE WHEN $3 = '' THEN macaddress ELSE $3 END, - discord_id = CASE WHEN $4 = '' THEN discord_id ELSE $4 END, - group_id = CASE WHEN $5 = 0 THEN group_id ELSE $5 END - WHERE id = $6 - RETURNING * - ", - ) - .bind(hostel) - .bind(year) - .bind(macaddress) - .bind(discord_id) - .bind(group_id) - .bind(id) - .fetch_one(pool.as_ref()) - .await?; - - Ok(member) - } - - //Mutation for adding attendance to the Attendance table - async fn add_attendance( - &self, - - ctx: &Context<'_>, - id: i32, - date: NaiveDate, - timein: NaiveTime, - timeout: NaiveTime, - is_present: bool, - ) -> Result { - let pool = ctx - .data::>() - .expect("Pool not found in context"); - - let attendance = sqlx::query_as::<_, Attendance>( - "INSERT INTO Attendance (id, date, timein, timeout, is_present) VALUES ($1, $2, $3, $4, $5) RETURNING *" - ) - .bind(id) - .bind(date) - .bind(timein) - .bind(timeout) - .bind(is_present) - .fetch_one(pool.as_ref()) - .await?; - - Ok(attendance) - } - - async fn mark_attendance( - &self, - ctx: &Context<'_>, - id: i32, - date: NaiveDate, - is_present: bool, - hmac_signature: String, - ) -> Result { - let pool = ctx - .data::>() - .expect("Pool not found in context"); - - let secret_key = ctx - .data::() - .expect("HMAC secret not found in context"); - - let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes()) - .expect("HMAC can take key of any size"); - - let message = format!("{}{}{}", id, date, is_present); - mac.update(message.as_bytes()); - - let expected_signature = mac.finalize().into_bytes(); - - // Convert the received HMAC signature from the client to bytes for comparison - let received_signature = hex::decode(hmac_signature) - .map_err(|_| sqlx::Error::Protocol("Invalid HMAC signature".into()))?; - - if expected_signature.as_slice() != received_signature.as_slice() { - return Err(sqlx::Error::Protocol("HMAC verification failed".into())); - } - - let current_time = Local::now().with_timezone(&Kolkata).time(); - - let attendance = sqlx::query_as::<_, Attendance>( - " - UPDATE Attendance - SET - timein = CASE WHEN timein = '00:00:00' THEN $1 ELSE timein END, - timeout = $1, - is_present = $2 - WHERE id = $3 AND date = $4 - RETURNING * - ", - ) - .bind(current_time) - .bind(is_present) - .bind(id) - .bind(date) - .fetch_one(pool.as_ref()) - .await?; - - Ok(attendance) - } - - async fn update_streak( - &self, - ctx: &Context<'_>, - id: i32, - has_sent_update: bool, - ) -> Result { - let pool = ctx - .data::>() - .expect("Pool not found in context"); - - let streak_info = sqlx::query_as::<_, StreakUpdate>( - " - SELECT id, streak, max_streak - FROM StreakUpdate - WHERE id = $1 - ", - ) - .bind(id) - .fetch_optional(pool.as_ref()) - .await?; - - match streak_info { - Some(mut member) => { - let current_streak = member.streak.unwrap_or(0); - let max_streak = member.max_streak.unwrap_or(0); - let (new_streak, new_max_streak) = if has_sent_update { - let updated_streak = current_streak + 1; - let updated_max_streak = updated_streak.max(max_streak); - (updated_streak, updated_max_streak) - } else { - (0, max_streak) - }; - let updated_member = sqlx::query_as::<_, StreakUpdate>( - " - UPDATE StreakUpdate - SET streak = $1, max_streak = $2 - WHERE id = $3 - RETURNING * - ", - ) - .bind(new_streak) - .bind(new_max_streak) - .bind(id) - .fetch_one(pool.as_ref()) - .await?; - - Ok(updated_member) - } - None => { - let new_member = sqlx::query_as::<_, StreakUpdate>( - " - INSERT INTO StreakUpdate (id, streak, max_streak) - VALUES ($1, $2, $3) - RETURNING * - ", - ) - .bind(id) - .bind(0) - .bind(0) - .fetch_one(pool.as_ref()) - .await?; - - Ok(new_member) - } - } - } -} diff --git a/src/graphql/mutations/attendance_mutations.rs b/src/graphql/mutations/attendance_mutations.rs new file mode 100644 index 0000000..aabfeb4 --- /dev/null +++ b/src/graphql/mutations/attendance_mutations.rs @@ -0,0 +1,41 @@ +use std::sync::Arc; + +use async_graphql::{Context, Object, Result}; +use chrono::Local; +use chrono_tz::Asia::Kolkata; +use sqlx::PgPool; + +use crate::models::attendance::{Attendance, MarkAttendanceInput}; + +#[derive(Default)] +pub struct AttendanceMutations; + +#[Object] +impl AttendanceMutations { + #[graphql(name = "markAttendance")] + async fn mark_attendance( + &self, + ctx: &Context<'_>, + input: MarkAttendanceInput, + ) -> Result { + let pool = ctx + .data::>() + .expect("Pool not found in context"); + + let now = Local::now().with_timezone(&Kolkata).date_naive(); + let attendance = sqlx::query_as::<_, Attendance>( + "INSERT INTO Attendance (member_id, date, is_present, time_in, time_out, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $6) RETURNING *", + ) + .bind(input.member_id) + .bind(input.date) + .bind(true) + .bind(input.time_in) + .bind(input.time_out) + .bind(now) + .fetch_one(pool.as_ref()) + .await?; + + Ok(attendance) + } +} diff --git a/src/graphql/mutations/member_mutations.rs b/src/graphql/mutations/member_mutations.rs new file mode 100644 index 0000000..f42dd10 --- /dev/null +++ b/src/graphql/mutations/member_mutations.rs @@ -0,0 +1,39 @@ +use std::sync::Arc; + +use async_graphql::{Context, Object, Result}; +use chrono::Local; +use chrono_tz::Asia::Kolkata; +use sqlx::PgPool; + +use crate::models::member::{CreateMemberInput, Member}; + +#[derive(Default)] +pub struct MemberMutations; + +#[Object] +impl MemberMutations { + #[graphql(name = "createMember")] + async fn create_member(&self, ctx: &Context<'_>, input: CreateMemberInput) -> Result { + let pool = ctx.data::>().expect("Pool must be in context."); + + let now = Local::now().with_timezone(&Kolkata).date_naive(); + let member = sqlx::query_as::<_, Member>( + "INSERT INTO Member (roll_no, name, email, sex, year, hostel, mac_address, discord_id, group_id, created_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *" + ) + .bind(&input.roll_no) + .bind(&input.name) + .bind(&input.email) + .bind(&input.sex) + .bind(input.year) + .bind(&input.hostel) + .bind(&input.mac_address) + .bind(&input.discord_id) + .bind(input.group_id) + .bind(now) + .fetch_one(pool.as_ref()) + .await?; + + Ok(member) + } +} diff --git a/src/graphql/mutations/mod.rs b/src/graphql/mutations/mod.rs new file mode 100644 index 0000000..b830bce --- /dev/null +++ b/src/graphql/mutations/mod.rs @@ -0,0 +1,3 @@ +pub mod attendance_mutations; +pub mod member_mutations; +pub mod streak_mutations; diff --git a/src/graphql/mutations/streak_mutations.rs b/src/graphql/mutations/streak_mutations.rs new file mode 100644 index 0000000..a4b9510 --- /dev/null +++ b/src/graphql/mutations/streak_mutations.rs @@ -0,0 +1,49 @@ +use std::sync::Arc; + +use async_graphql::{Context, Error, Object, Result}; +use sqlx::PgPool; + +use crate::models::status_update_streak::{IncrementStreakInput, StatusUpdateStreak as Streak}; + +#[derive(Default)] +pub struct StreakMutations; + +#[Object] +impl StreakMutations { + #[graphql(name = "incrementStreak")] + async fn increment_streak( + &self, + ctx: &Context<'_>, + input: IncrementStreakInput, + ) -> Result { + let pool = ctx.data::>().expect("Pool must be in context."); + + // Ensure at least one identifier is provided + if input.member_id.is_none() && input.discord_id.is_none() { + return Err(Error::new( + "Either `member_id` or `discord_id` must be provided.", + )); + } + + let mut sql = String::from("UPDATE Streaks SET current_streak = current_streak + 1, max_streak = GREATEST(max_streak, current_streak + 1) WHERE "); + if let Some(_) = input.member_id { + sql.push_str("member_id = $1"); + } else if let Some(_) = input.discord_id { + sql.push_str("discord_id = $1"); + } + + sql.push_str(" RETURNING *"); + + let query = if let Some(member_id) = input.member_id { + sqlx::query_as::<_, Streak>(&sql).bind(member_id) + } else if let Some(discord_id) = input.discord_id { + sqlx::query_as::<_, Streak>(&sql).bind(discord_id) + } else { + return Err(Error::new("Invalid input.")); + }; + + let updated_streak = query.fetch_one(pool.as_ref()).await?; + + Ok(updated_streak) + } +} diff --git a/src/graphql/queries/attendance_queries.rs b/src/graphql/queries/attendance_queries.rs new file mode 100644 index 0000000..576fdab --- /dev/null +++ b/src/graphql/queries/attendance_queries.rs @@ -0,0 +1,69 @@ +use std::sync::Arc; + +use async_graphql::{Context, Object, Result}; +use sqlx::PgPool; + +use crate::models::{attendance::Attendance, member::Member}; + +/// Sub-query for the [`Attendance`] table. The queries are: +/// * attendance - get a specific member's attendance details using their member_id, roll_no or discord_id +#[derive(Default)] +pub struct AttendanceQueries; + +#[Object] +impl AttendanceQueries { + async fn attendance( + &self, + ctx: &Context<'_>, + member_id: Option, + roll_no: Option, + discord_id: Option, + ) -> Result> { + let pool = ctx.data::>().expect("Pool must be in context."); + + // member_id is given, simple query + if let Some(id) = member_id { + let attendance_query = + sqlx::query_as::<_, Attendance>("SELECT * FROM Attendance WHERE member_id = $1") + .bind(id) + .fetch_all(pool.as_ref()) + .await?; + + return Ok(attendance_query); + } + + // Get the member using their roll_no or discord_id + let member_query = if let Some(roll) = roll_no { + sqlx::query_as::<_, Member>("SELECT * FROM Member WHERE roll_no = $1") + .bind(roll) + .fetch_one(pool.as_ref()) + .await + } else if let Some(discord) = discord_id { + sqlx::query_as::<_, Member>("SELECT * FROM Member WHERE discord_id = $1") + .bind(discord) + .fetch_one(pool.as_ref()) + .await + } else { + return Err(async_graphql::Error::new( + "At least one key (member_id, roll_no, discord_id) must be specified.", + )); + }; + + let member = match member_query { + Ok(member) => member, + Err(_) => { + return Err(async_graphql::Error::new( + "No member found with the given criteria.", + )) + } + }; + + let attendance_query = + sqlx::query_as::<_, Attendance>("SELECT * FROM Attendance WHERE member_id = $1") + .bind(member.member_id) + .fetch_all(pool.as_ref()) + .await?; + + Ok(attendance_query) + } +} diff --git a/src/graphql/queries/member_queries.rs b/src/graphql/queries/member_queries.rs new file mode 100644 index 0000000..beb6520 --- /dev/null +++ b/src/graphql/queries/member_queries.rs @@ -0,0 +1,128 @@ +use async_graphql::{ComplexObject, Context, Object, Result}; +use sqlx::PgPool; +use std::sync::Arc; + +use crate::models::{ + attendance::{AttendanceInfo, AttendanceSummaryInfo}, + member::Member, + status_update_streak::StatusUpdateStreakInfo, +}; + +/// Sub-query for the [`Member`] table. The queries are: +/// * members - get list of all members and their details, optionally filtered by year and id +/// * member - get a specific member's details using their id, roll_no or discord_id +#[derive(Default)] +pub struct MemberQueries; + +#[Object] +impl MemberQueries { + pub async fn member( + &self, + ctx: &Context<'_>, + member_id: Option, + roll_no: Option, + discord_id: Option, + ) -> Result { + let pool = ctx.data::>().expect("Pool must be in context."); + + // Base SQL query + let mut sql = String::from("SELECT * FROM Member WHERE "); + + // Push filter + let query = if let Some(id) = member_id { + sql.push_str("member_id = $1"); + sqlx::query_as::<_, Member>(&sql).bind(id) + } else if let Some(roll) = roll_no { + sql.push_str("roll_no = $1"); + sqlx::query_as::<_, Member>(&sql).bind(roll) + } else if let Some(discord) = discord_id { + sql.push_str("discord_id = $1"); + sqlx::query_as::<_, Member>(&sql).bind(discord) + } else { + return Err(async_graphql::Error::new( + "At least one key must be specified.", + )); + }; + + let result = query.fetch_one(pool.as_ref()).await?; + Ok(result) + } + + pub async fn members( + &self, + ctx: &Context<'_>, + year: Option, + group_id: Option, + ) -> Result> { + let pool = ctx.data::>().expect("Pool must be in context."); + + // Base standalone query + let mut query = sqlx::QueryBuilder::new("SELECT * FROM Member WHERE 1=1"); + + // Push filters if necessary + if let Some(y) = year { + query.push(" AND year = "); + query.push_bind(y); + } + + if let Some(g) = group_id { + query.push(" AND group_id = "); + query.push_bind(g); + } + + let members = query + .build_query_as::() + .fetch_all(pool.as_ref()) + .await?; + + Ok(members) + } +} + +/// These are resolvers to handle nested queries i.e if the end-user wants details of members held in other tables. Currently you can query the following for each Member: +/// * date, is_present, time_in and time_out fields of [`Attendance`] +/// * year, month and days_attendned fields of [`AttendanceSummary`] +/// * current_streak and max_streak fields of [`StatusUpdateStreak`] +/// Resolvers can be easily changed to show more fields by editing the SQLx query in their respective methods. +#[ComplexObject] +impl Member { + async fn attendance(&self, ctx: &Context<'_>) -> Vec { + let pool = ctx.data::>().expect("Pool must be in context."); + let query = sqlx::query_as::<_, AttendanceInfo>( + "SELECT date, is_present, time_in, time_out FROM Attendance WHERE member_id = $1", + ) + .bind(self.member_id) + .fetch_all(pool.as_ref()) + .await + .unwrap_or_default(); + + query + } + + #[graphql(name = "attendanceSummary")] + async fn attendance_summary(&self, ctx: &Context<'_>) -> Vec { + let pool = ctx.data::>().expect("Pool must be in context."); + let query = sqlx::query_as::<_, AttendanceSummaryInfo>( + "SELECT year, month, days_attended FROM AttendanceSummary WHERE member_id = $1", + ) + .bind(self.member_id) + .fetch_all(pool.as_ref()) + .await + .unwrap_or_default(); + + query + } + + async fn streak(&self, ctx: &Context<'_>) -> Vec { + let pool = ctx.data::>().expect("Pool must be in context."); + let query = sqlx::query_as::<_, StatusUpdateStreakInfo>( + "SELECT current_streak, max_streak FROM StatusUpdateStreak WHERE member_id = $1", + ) + .bind(self.member_id) + .fetch_all(pool.as_ref()) + .await + .unwrap_or_default(); + + query + } +} diff --git a/src/graphql/queries/mod.rs b/src/graphql/queries/mod.rs new file mode 100644 index 0000000..2dbdb1f --- /dev/null +++ b/src/graphql/queries/mod.rs @@ -0,0 +1,3 @@ +pub mod attendance_queries; +pub mod member_queries; +pub mod streak_queries; diff --git a/src/graphql/queries/streak_queries.rs b/src/graphql/queries/streak_queries.rs new file mode 100644 index 0000000..b9cb41f --- /dev/null +++ b/src/graphql/queries/streak_queries.rs @@ -0,0 +1,70 @@ +use std::sync::Arc; + +use crate::models::{member::Member, status_update_streak::StatusUpdateStreak as Streak}; +use async_graphql::{Context, Object, Result}; +use sqlx::PgPool; + +/// Sub-query for the [`Streak`] table. The queries are: +/// * streak - get a specific member's attendance details using their member_id, roll_no or discord_id +#[derive(Default)] +pub struct StreakQueries; + +#[Object] +impl StreakQueries { + async fn streak( + &self, + ctx: &Context<'_>, + member_id: Option, + roll_no: Option, + discord_id: Option, + ) -> Result { + let pool = ctx.data::>().expect("Pool must be in context."); + + // member_id is given, simple query + if let Some(id) = member_id { + let streak_query = sqlx::query_as::<_, Streak>( + "SELECT current_streak, max_streak FROM StatusUpdateStreak WHERE member_id = $1", + ) + .bind(id) + .fetch_one(pool.as_ref()) + .await?; + + return Ok(streak_query); + } + + // Get the member using their roll_no or discord_id + let member_query = if let Some(roll) = roll_no { + sqlx::query_as::<_, Member>("SELECT * FROM Member WHERE roll_no = $1") + .bind(roll) + .fetch_one(pool.as_ref()) + .await + } else if let Some(discord) = discord_id { + sqlx::query_as::<_, Member>("SELECT * FROM Member WHERE discord_id = $1") + .bind(discord) + .fetch_one(pool.as_ref()) + .await + } else { + return Err(async_graphql::Error::new( + "At least one key (member_id, roll_no, discord_id) must be specified.", + )); + }; + + let member = match member_query { + Ok(member) => member, + Err(_) => { + return Err(async_graphql::Error::new( + "No member found with the given criteria.", + )) + } + }; + + let streak_query = sqlx::query_as::<_, Streak>( + "SELECT current_streak, max_streak FROM Streak WHERE member_id = $1", + ) + .bind(member.member_id) + .fetch_one(pool.as_ref()) + .await?; + + Ok(streak_query) + } +} diff --git a/src/graphql/query.rs b/src/graphql/query.rs deleted file mode 100644 index 98024c1..0000000 --- a/src/graphql/query.rs +++ /dev/null @@ -1,168 +0,0 @@ -use async_graphql::{Context, Object}; -use chrono::NaiveDate; -use root::models::{ - attendance::Attendance, - member::Member, -}; -use sqlx::PgPool; -use std::sync::Arc; - -pub struct QueryRoot; - -#[Object] -impl QueryRoot { - async fn get_member(&self, ctx: &Context<'_>) -> Result, sqlx::Error> { - let pool = ctx - .data::>() - .expect("Pool not found in context"); - let users = sqlx::query_as::<_, Member>("SELECT * FROM Member") - .fetch_all(pool.as_ref()) - .await?; - Ok(users) - } - - //Query for retrieving the attendance based on date - async fn get_attendance( - &self, - ctx: &Context<'_>, - date: NaiveDate, - ) -> Result, sqlx::Error> { - let pool = ctx - .data::>() - .expect("Pool not found in context"); - - let attendance_list = sqlx::query_as::<_, Attendance>( - "SELECT id, date, timein, timeout, is_present - FROM Attendance WHERE date = $1", - ) - .bind(date) - .fetch_all(pool.as_ref()) - .await?; - Ok(attendance_list) - } - async fn get_streak(&self, ctx: &Context<'_>, id: i32) -> Result { - let pool = ctx - .data::>() - .expect("Pool not found in context"); - let streak = sqlx::query_as::<_, StreakUpdate>("SELECT * FROM StreakUpdate WHERE id = $1") - .bind(id) - .fetch_one(pool.as_ref()) - .await?; - - Ok(streak) - } - - async fn get_update_streak(&self, ctx: &Context<'_>) -> Result, sqlx::Error> { - let pool = ctx - .data::>() - .expect("Pool not found in context"); - let streak = sqlx::query_as::<_, StreakUpdate>("SELECT * FROM StreakUpdate") - .fetch_all(pool.as_ref()) - .await?; - - Ok(streak) - } - - async fn get_attendance_streak( - &self, - ctx: &Context<'_>, - start_date: NaiveDate, - end_date: NaiveDate, - ) -> Result, sqlx::Error> { - let pool = ctx - .data::>() - .expect("Pool not found in context"); - let attendance_streak = sqlx::query_as::<_, AttendanceStreak>( - "SELECT * from AttendanceStreak - WHERE month >= $1 AND month < $2 - ", - ) - .bind(start_date) - .bind(end_date) - .fetch_all(pool.as_ref()) - .await?; - - Ok(attendance_streak) - } - - async fn get_attendance_summary( - &self, - ctx: &Context<'_>, - start_date: NaiveDate, - end_date: NaiveDate, - ) -> Result { - let pool = ctx - .data::>() - .expect("Pool not found in context"); - let attendance_days = sqlx::query_as::<_, (NaiveDate, i64)>( - "SELECT date, COUNT(*) FROM Attendance - WHERE date >= $1 AND date < $2 - AND is_present = true - GROUP BY date ORDER BY date", - ) - .bind(start_date) - .bind(end_date) - .fetch_all(pool.as_ref()) - .await?; - - let member_attendance = sqlx::query_as::<_, (i32, i64)>( - "SELECT id, COUNT(*) FROM Attendance - WHERE date >= $1 AND date < $2 - AND is_present = true - GROUP BY id", - ) - .bind(start_date) - .bind(end_date) - .fetch_all(pool.as_ref()) - .await?; - - let max_count = sqlx::query_scalar::<_, i64>( - "SELECT COUNT(*) FROM Attendance - WHERE date >= $1 AND date < $2 - AND is_present = true", - ) - .bind(start_date) - .bind(end_date) - .fetch_all(pool.as_ref()) - .await?; - - let daily_count = attendance_days - .into_iter() - .map(|(date, count)| DailyCount { date, count }) - .collect(); - - let member_attendance = member_attendance - .into_iter() - .map(|(id, present_days)| MemberAttendance { id, present_days }) - .collect(); - - let summaries: AttendanceSummary = AttendanceSummary { - max_days: max_count[0], - member_attendance, - daily_count, - }; - - Ok(summaries) - } - - pub async fn get_non_working_days( - &self, - ctx: &Context<'_>, - ) -> Result, sqlx::Error> { - let pool = ctx - .data::>() - .expect("Pool not found in context"); - - let dates = sqlx::query_scalar::<_, NaiveDate>( - "SELECT date - FROM Attendance - GROUP BY date - HAVING BOOL_AND(NOT is_present) - ORDER BY date", - ) - .fetch_all(pool.as_ref()) - .await?; - - Ok(dates) - } -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 19aefd8..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -// Need this to expose models module for tests -pub mod attendance; -pub mod models; diff --git a/src/main.rs b/src/main.rs index c0cdaa4..2419001 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ -use async_graphql::EmptySubscription; +use async_graphql::MergedObject; +use async_graphql::{EmptyMutation, EmptySubscription}; use async_graphql_axum::GraphQL; use axum::{ http::{HeaderValue, Method}, @@ -6,16 +7,39 @@ use axum::{ Router, }; use chrono_tz::Asia::Kolkata; -use graphql::{mutations::MutationRoot, query::QueryRoot}; -use root::attendance::daily_task; +use daily_task::daily_task::execute_daily_task; +use graphql::mutations::attendance_mutations::AttendanceMutations; +use graphql::{ + mutations::member_mutations::MemberMutations, + queries::{ + attendance_queries::AttendanceQueries, member_queries::MemberQueries, + streak_queries::StreakQueries, + }, +}; use sqlx::PgPool; use std::sync::Arc; use tokio::time::sleep_until; use tower_http::cors::CorsLayer; use tracing::info; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::{fmt, EnvFilter}; + +/// Daily task contains the function that is executed daily at midnight, using the thread spawned in main(). +pub mod daily_task; +/// This module handles all logic for queries and mutations, based on the [`crate::models`]. Each sub-module maps to one table in the DB. +pub mod graphql; +/// These models not only help SQLx map it to the relational DB, but is also used by async_graphql to define its resolvers for queries and mutations. +pub mod models; +/// Since we really only need one route for a GraphQL server, this just holds a function returning the GraphiQL playground. Probably can clean this up later. +pub mod routes; + +// This is our main query or QueryRoot. It is made up of structs representing sub-queries, one for each table in the DB. The fields of a relation are exposed via the [`async_graphql::SimpleObject`] directive on the [`models`] themselves. Specific queries, such as getting a member by ID or getting the streak of a member is defined as methods of the sub-query struct. Complex queries, such as those getting related data from multiple tables like querying all members and the streaks of each member, are defined via the [`async_graphql::ComplexObject`] directive on the [`models`] and can be found in the corresponding sub-query module. +#[derive(MergedObject, Default)] +struct Query(MemberQueries, AttendanceQueries, StreakQueries); -mod graphql; -mod routes; +#[derive(MergedObject, Default)] +struct Mutations(MemberMutations, AttendanceMutations); #[tokio::main] async fn main() { @@ -23,14 +47,41 @@ async fn main() { // 9/1/25: TODO: Explain? // env::set_var("PGOPTIONS", "-c ignore_version=true"); - tracing_subscriber::fmt::init(); // Currently, we need the DATABASE_URL to be loaded in through the .env. // In the future, if we use any other configuration (say Github Secrets), we // can allow dotenv() to err. dotenv::dotenv().expect("Failed to load .env file."); + // Used to check if it's in production + let env = std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()); let secret_key = std::env::var("ROOT_SECRET").expect("ROOT_SECRET must be set."); let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set."); + + if env == "development" { + tracing_subscriber::registry() + .with(fmt::layer().pretty().with_writer(std::io::stdout)) + .with( + fmt::layer() + .pretty() + .with_ansi(false) + .with_writer(std::fs::File::create("root.log").unwrap()), + ) + .with(EnvFilter::new("trace")) + .init(); + info!("Running in development mode."); + } else { + tracing_subscriber::registry() + .with( + fmt::layer() + .pretty() + .with_ansi(false) + .with_writer(std::fs::File::create("root.log").unwrap()), + ) + .with(EnvFilter::new("info")) + .init(); + info!("Running in production mode.") + } + let pool = sqlx::postgres::PgPoolOptions::new() .min_connections(2) // Maintain at least two connections, one for amD and one for Home. It should be .max_connections(3) // pretty unlikely that amD, Home and the web interface is used simultaneously @@ -46,10 +97,11 @@ async fn main() { // Wrap pool in an Arc to share across threads let pool = Arc::new(pool); - let schema = async_graphql::Schema::build(QueryRoot, MutationRoot, EmptySubscription) - .data(pool.clone()) - .data(secret_key) - .finish(); + let schema = + async_graphql::Schema::build(Query::default(), Mutations::default(), EmptySubscription) + .data(pool.clone()) + .data(secret_key) + .finish(); // This thread will sleep until it's time to run the daily task // Also takes ownership of pool @@ -90,6 +142,6 @@ async fn run_daily_task_at_midnight(pool: Arc) { tokio::time::Duration::from_secs(duration_until_midnight.num_seconds() as u64); sleep_until(tokio::time::Instant::now() + sleep_duration).await; - daily_task::execute_daily_task(pool.clone()).await; + execute_daily_task(pool.clone()).await; } } diff --git a/src/models/attendance.rs b/src/models/attendance.rs index 91a181f..7faa7ed 100644 --- a/src/models/attendance.rs +++ b/src/models/attendance.rs @@ -1,22 +1,50 @@ -use async_graphql::SimpleObject; +use async_graphql::{InputObject, SimpleObject}; +use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; use sqlx::FromRow; -#[derive(FromRow, SimpleObject, Debug)] +#[derive(SimpleObject, FromRow)] pub struct Attendance { pub attendance_id: i32, pub member_id: i32, - pub date: chrono::NaiveDate, + pub date: NaiveDate, pub is_present: bool, - pub time_in: Option, - pub time_out: Option, - pub created_at: chrono::NaiveDateTime, - pub updated_at: chrono::NaiveDateTime, + pub time_in: Option, + pub time_out: Option, + #[graphql(skip)] // Don't expose internal fields/meta-data + pub created_at: NaiveDateTime, + #[graphql(skip)] + pub updated_at: NaiveDateTime, } -#[derive(FromRow, SimpleObject, Debug)] +#[derive(SimpleObject, FromRow)] pub struct AttendanceSummary { pub member_id: i32, pub year: i32, pub month: i32, pub days_attended: i32, } + +/// This struct is used in place of [`Attendance`] in nested queries to avoid overfetching. +#[derive(SimpleObject, FromRow)] +pub struct AttendanceInfo { + pub date: NaiveDate, + pub is_present: bool, + pub time_in: Option, + pub time_out: Option, +} + +/// This struct is used in place of [`AttendanceSummary`] in nested queries to avoid overfetching. +#[derive(SimpleObject, FromRow)] +pub struct AttendanceSummaryInfo { + pub year: i32, + pub month: i32, + pub days_attended: i32, +} + +#[derive(InputObject)] +pub struct MarkAttendanceInput { + pub member_id: i32, + pub date: NaiveDate, + pub time_in: Option, + pub time_out: Option, +} diff --git a/src/models/member.rs b/src/models/member.rs index 03e47eb..e70478c 100644 --- a/src/models/member.rs +++ b/src/models/member.rs @@ -1,9 +1,34 @@ -use async_graphql::SimpleObject; +use async_graphql::{Enum, InputObject, SimpleObject}; +use chrono::NaiveDateTime; use sqlx::FromRow; -#[derive(FromRow, SimpleObject)] +#[derive(Enum, Copy, Clone, Eq, PartialEq, sqlx::Type)] +#[sqlx(type_name = "sex_type")] +pub enum Sex { + M, + F, + Other, +} + +#[derive(SimpleObject, FromRow)] +#[graphql(complex)] pub struct Member { pub member_id: i32, + pub roll_no: String, + pub name: String, + pub email: String, + pub sex: Sex, + pub year: i32, + pub hostel: String, + pub mac_address: String, + pub discord_id: String, + pub group_id: i32, + #[graphql(skip)] // Don't expose internal fields/meta-data + pub created_at: NaiveDateTime, +} + +#[derive(InputObject)] +pub struct CreateMemberInput { pub roll_no: String, pub name: String, pub email: String, @@ -13,5 +38,4 @@ pub struct Member { pub mac_address: String, pub discord_id: String, pub group_id: i32, - pub created_at: chrono::NaiveDateTime, } diff --git a/src/models/mod.rs b/src/models/mod.rs index 026cf0a..e23fcc7 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,2 +1,3 @@ pub mod attendance; pub mod member; +pub mod status_update_streak; diff --git a/src/models/status_update_streak.rs b/src/models/status_update_streak.rs new file mode 100644 index 0000000..4cb3451 --- /dev/null +++ b/src/models/status_update_streak.rs @@ -0,0 +1,21 @@ +use async_graphql::{InputObject, SimpleObject}; +use sqlx::FromRow; + +#[derive(SimpleObject, FromRow)] +pub struct StatusUpdateStreak { + pub member_id: i32, + pub current_streak: i32, + pub max_streak: i32, +} + +#[derive(SimpleObject, FromRow)] +pub struct StatusUpdateStreakInfo { + pub current_streak: i32, + pub max_streak: i32, +} + +#[derive(InputObject)] +pub struct IncrementStreakInput { + pub member_id: Option, + pub discord_id: Option, +} diff --git a/src/routes.rs b/src/routes.rs index f94d6b3..1ec4c07 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,6 +1,8 @@ use async_graphql::http::GraphiQLSource; use axum::response::{Html, IntoResponse}; +// TODO: We do not want to expose GraphiQL unless in dev. +/// Returns the built-in GraphQL playground from async_graphql. pub async fn graphiql() -> impl IntoResponse { Html( GraphiQLSource::build() From ba213736894353af5c0336e8c87aafb118118069 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Fri, 17 Jan 2025 12:46:10 +0530 Subject: [PATCH 23/38] remove tests --- tests/integration_tests.rs | 278 ------------------------------------- 1 file changed, 278 deletions(-) delete mode 100644 tests/integration_tests.rs diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs deleted file mode 100644 index 8823679..0000000 --- a/tests/integration_tests.rs +++ /dev/null @@ -1,278 +0,0 @@ -use root::db::leaderboard::Leaderboard; -use root::db::member::Member; -use root::leaderboard::fetch_stats::{fetch_codeforces_stats, fetch_leetcode_stats}; -use root::leaderboard::update_leaderboard::update_leaderboard; -use sqlx::{postgres::PgPoolOptions, PgPool}; -use std::env; -use std::sync::Arc; - -pub fn get_database_url() -> String { - match env::var("TEST_DATABASE_URL") { - Ok(db_url) => db_url, - Err(_) => "postgres://localhost:5432/default_db".to_string(), - } -} - -// Helper function to create a test database connection -async fn setup_test_db() -> PgPool { - let database_url = get_database_url(); - let pool = PgPoolOptions::new() - .max_connections(5) - .connect(&database_url) - .await - .expect("Failed to create test database pool"); - - // Create tables if they do not already exist - let queries = vec![ - r#" - CREATE TABLE IF NOT EXISTS member ( - id SERIAL PRIMARY KEY, - rollno VARCHAR(255) NOT NULL, - name VARCHAR(255) NOT NULL, - hostel VARCHAR(255) NOT NULL, - email VARCHAR(255) NOT NULL UNIQUE, - sex VARCHAR(10) NOT NULL, - year INT NOT NULL, - macaddress VARCHAR(17) NOT NULL, - discord_id VARCHAR(255), - group_id INT NOT NULL - )"#, - r#" - CREATE TABLE IF NOT EXISTS leaderboard ( - id SERIAL PRIMARY KEY, - member_id INT UNIQUE NOT NULL, - leetcode_score INT, - codeforces_score INT, - unified_score INT NOT NULL, - last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (member_id) REFERENCES member(id) - )"#, - r#" - CREATE TABLE IF NOT EXISTS leetcode_stats ( - id SERIAL PRIMARY KEY, - member_id INT UNIQUE NOT NULL, - leetcode_username VARCHAR(255) NOT NULL, - problems_solved INT NOT NULL, - easy_solved INT NOT NULL, - medium_solved INT NOT NULL, - hard_solved INT NOT NULL, - contests_participated INT NOT NULL, - best_rank INT NOT NULL, - total_contests INT NOT NULL, - FOREIGN KEY (member_id) REFERENCES member(id) - )"#, - r#" - CREATE TABLE IF NOT EXISTS codeforces_stats ( - id SERIAL PRIMARY KEY, - member_id INT UNIQUE NOT NULL, - codeforces_handle VARCHAR(255) NOT NULL, - codeforces_rating INT NOT NULL, - max_rating INT NOT NULL, - contests_participated INT NOT NULL, - FOREIGN KEY (member_id) REFERENCES member(id) - )"#, - ]; - - for query in queries { - sqlx::query(query) - .execute(&pool) - .await - .expect("Failed to execute query"); - } - pool -} - -// Helper function to clean up test data - -async fn cleanup_test_data(pool: &PgPool) { - print!("called"); - let cleanup_query = r#" - DO $$ - DECLARE - seq RECORD; - BEGIN - -- Droppign all the tables for cleanup purpose - BEGIN - TRUNCATE TABLE leaderboard, leetcode_stats, codeforces_stats, member RESTART IDENTITY CASCADE; - EXCEPTION - WHEN undefined_table THEN - -- Ignore errors if tables don't exist - RAISE NOTICE 'Tables do not exist, skipping TRUNCATE.'; - END; - - -- Postgres stores the sequences of unique id outside of respective tables, so need to delete those too. - FOR seq IN - SELECT c.relname - FROM pg_class c - JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind = 'S' AND n.nspname = 'public' - LOOP - BEGIN - EXECUTE 'ALTER SEQUENCE ' || seq.relname || ' RESTART WITH 1'; - EXCEPTION - WHEN undefined_object THEN - -- Ignore errors if sequences don't exist - RAISE NOTICE 'Sequence % does not exist, skipping.', seq.relname; - END; - END LOOP; - END $$; - "#; - - sqlx::query(cleanup_query) - .execute(pool) - .await - .expect("Failed to clean up and reset database state"); -} - -#[tokio::test] -// Additional helper test to verify database connections and basic operations -async fn test_database_connection() { - let database_url = get_database_url(); - println!("Database URL: {}", database_url); - assert!(!database_url.is_empty(), "Database URL should not be empty"); -} - -//test -#[tokio::test] -async fn test_insert_members_and_update_stats() { - let pool = setup_test_db().await; - - // Define test members - let members = vec![ - ( - "B21CS1234", - "John Doe", - "Hostel A", - "john.doe@example.com", - "Male", - 2021, - "00:11:22:33:44:55", - Some("john_discord"), - 1, - "swayam-agrahari", - "tourist", - ), - ( - "B21CS5678", - "Jane Smith", - "Hostel B", - "jane.smith@example.com", - "Female", - 2021, - "66:77:88:99:AA:BB", - Some("jane_discord"), - 2, - "rihaan1810", - "tourist", - ), - ]; - - let mut inserted_members = Vec::new(); - - // Insert members and store their IDs - for member in &members { - // Insert Member - let member_result = sqlx::query_as::<_, Member>( - "INSERT INTO member (rollno, name, hostel, email, sex, year, macaddress, discord_id, group_id) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) - RETURNING *", - ) - .bind(&member.0) - .bind(&member.1) - .bind(&member.2) - .bind(&member.3) - .bind(&member.4) - .bind(member.5) - .bind(&member.6) - .bind(&member.7) - .bind(&member.8) - .fetch_one(&pool) - .await - .expect("Failed to insert member"); - - // Insert LeetCode stats - let _leetcode_result = sqlx::query( - "INSERT INTO leetcode_stats (member_id, leetcode_username,problems_solved,easy_solved,medium_solved,hard_solved,contests_participated,best_rank,total_contests) - VALUES ($1, $2, 0,0,0,0,0,0,0)", - ) - .bind(member_result.id) - .bind(&member.9) - .execute(&pool) - .await - .expect("Failed to insert LeetCode stats"); - - // Insert Codeforces stats - let _codeforces_result = sqlx::query( - "INSERT INTO codeforces_stats (member_id, codeforces_handle,codeforces_rating,max_rating,contests_participated) - VALUES ($1, $2, 0,0,0)", - ) - .bind(member_result.id) - .bind(&member.10) - .execute(&pool) - .await - .expect("Failed to insert Codeforces stats"); - - inserted_members.push(member_result.id); - } - - // Test LeetCode stats fetching - for (member_id, leetcode_username) in inserted_members.iter().zip(members.iter().map(|m| m.9)) { - match fetch_leetcode_stats(Arc::new(pool.clone()), *member_id, leetcode_username).await { - Ok(_) => println!( - "Successfully fetched LeetCode stats for member ID: {}", - member_id - ), - Err(e) => { - println!("Error fetching LeetCode stats: {:?}", e); - // Uncomment to fail test on fetch error - // panic!("Failed to fetch LeetCode stats") - } - } - } - - // Test Codeforces stats fetching - for (member_id, codeforces_handle) in inserted_members.iter().zip(members.iter().map(|m| m.9)) { - match fetch_codeforces_stats(Arc::new(pool.clone()), *member_id, codeforces_handle).await { - Ok(_) => println!( - "Successfully fetched Codeforces stats for member ID: {}", - member_id - ), - Err(e) => { - println!("Error fetching Codeforces stats: {:?}", e); - } - } - } - - // Test leaderboard update - match update_leaderboard(Arc::new(pool.clone())).await { - Ok(_) => println!("Successfully updated leaderboard"), - Err(e) => panic!("Failed to update leaderboard: {:?}", e), - } - - // Verify leaderboard entries - let leaderboard_entries = sqlx::query_as::<_, Leaderboard>("SELECT * FROM leaderboard") - .fetch_all(&pool) - .await - .unwrap(); - - assert_eq!( - leaderboard_entries.len(), - 2, - "Should have 2 leaderboard entries" - ); - - // Assertions about leaderboard scores - for entry in leaderboard_entries { - assert!(entry.unified_score > 0, "Unified score should be positive"); - assert!( - entry.leetcode_score.is_some(), - "LeetCode score should be set" - ); - assert!( - entry.codeforces_score.is_some(), - "Codeforces score should be set" - ); - } - - cleanup_test_data(&pool).await; -} From e6657daba95f897754c82209d170baadef9e68ef Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Fri, 17 Jan 2025 13:16:54 +0530 Subject: [PATCH 24/38] add re-exports for less verbose imports --- src/daily_task/mod.rs | 2 ++ src/graphql/mutations/mod.rs | 4 ++++ src/graphql/queries/mod.rs | 4 ++++ src/main.rs | 27 +++++++++++---------------- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/daily_task/mod.rs b/src/daily_task/mod.rs index 884e79f..993b199 100644 --- a/src/daily_task/mod.rs +++ b/src/daily_task/mod.rs @@ -1 +1,3 @@ pub mod daily_task; + +pub use daily_task::execute_daily_task; diff --git a/src/graphql/mutations/mod.rs b/src/graphql/mutations/mod.rs index b830bce..3fa4c30 100644 --- a/src/graphql/mutations/mod.rs +++ b/src/graphql/mutations/mod.rs @@ -1,3 +1,7 @@ pub mod attendance_mutations; pub mod member_mutations; pub mod streak_mutations; + +pub use attendance_mutations::AttendanceMutations; +pub use member_mutations::MemberMutations; +pub use streak_mutations::StreakMutations; diff --git a/src/graphql/queries/mod.rs b/src/graphql/queries/mod.rs index 2dbdb1f..1f416a4 100644 --- a/src/graphql/queries/mod.rs +++ b/src/graphql/queries/mod.rs @@ -1,3 +1,7 @@ pub mod attendance_queries; pub mod member_queries; pub mod streak_queries; + +pub use attendance_queries::AttendanceQueries; +pub use member_queries::MemberQueries; +pub use streak_queries::StreakQueries; diff --git a/src/main.rs b/src/main.rs index 2419001..30c403d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ -use async_graphql::MergedObject; -use async_graphql::{EmptyMutation, EmptySubscription}; +use async_graphql::{EmptySubscription, MergedObject}; use async_graphql_axum::GraphQL; use axum::{ http::{HeaderValue, Method}, @@ -7,23 +6,18 @@ use axum::{ Router, }; use chrono_tz::Asia::Kolkata; -use daily_task::daily_task::execute_daily_task; -use graphql::mutations::attendance_mutations::AttendanceMutations; -use graphql::{ - mutations::member_mutations::MemberMutations, - queries::{ - attendance_queries::AttendanceQueries, member_queries::MemberQueries, - streak_queries::StreakQueries, - }, -}; use sqlx::PgPool; use std::sync::Arc; use tokio::time::sleep_until; use tower_http::cors::CorsLayer; use tracing::info; -use tracing_subscriber::layer::SubscriberExt; -use tracing_subscriber::util::SubscriberInitExt; -use tracing_subscriber::{fmt, EnvFilter}; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; + +use daily_task::execute_daily_task; +use graphql::{ + mutations::{AttendanceMutations, MemberMutations, StreakMutations}, + queries::{AttendanceQueries, MemberQueries, StreakQueries}, +}; /// Daily task contains the function that is executed daily at midnight, using the thread spawned in main(). pub mod daily_task; @@ -38,12 +32,13 @@ pub mod routes; #[derive(MergedObject, Default)] struct Query(MemberQueries, AttendanceQueries, StreakQueries); +// Mutations work the same as Queries, sub-modules for each relation in the DB. However, all methods are directly defined on these sub-module structs. But they use slightly modified versions of the [`models`], marked by the Input in the name, to get input. #[derive(MergedObject, Default)] -struct Mutations(MemberMutations, AttendanceMutations); +struct Mutations(MemberMutations, AttendanceMutations, StreakMutations); #[tokio::main] async fn main() { - // 12/1/25: Going to assume this is only necessary for shuttle + // 12/1/25: Going to assume this is only necessary for shuttle. // 9/1/25: TODO: Explain? // env::set_var("PGOPTIONS", "-c ignore_version=true"); From 76ac3259fa245c0c02d2d804acedc025573143a1 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Fri, 17 Jan 2025 13:18:17 +0530 Subject: [PATCH 25/38] add BIND_ADDRESS env variable to avoid hardcoding address --- src/main.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 30c403d..1250c8d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -51,6 +51,8 @@ async fn main() { let env = std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()); let secret_key = std::env::var("ROOT_SECRET").expect("ROOT_SECRET must be set."); let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set."); + // BIND_ADDRESS is used to determine the IP address for the server's socket to bind to. + let bind_addr = std::env::var("BIND_ADDRESS").expect("BIND_ADDRESS must be set."); if env == "development" { tracing_subscriber::registry() @@ -118,8 +120,7 @@ async fn main() { ) .layer(cors); - // TODO: Replace hardcoded address - let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); + let listener = tokio::net::TcpListener::bind(bind_addr).await.unwrap(); axum::serve(listener, router).await.unwrap(); } From 3ba685d442828e1af99d5480123050ac7e1a3b84 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Fri, 17 Jan 2025 13:18:49 +0530 Subject: [PATCH 26/38] improve documentation in main.rs --- src/main.rs | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1250c8d..f1b0cc5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -47,41 +47,48 @@ async fn main() { // can allow dotenv() to err. dotenv::dotenv().expect("Failed to load .env file."); - // Used to check if it's in production + // RUST_ENV is used to check if it's in production to avoid unnecessary logging and exposing the + // graphiql interface. Make sure to set it to "production" before deployment. let env = std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()); + // ROOT_SECRET is used to cryptographically verify the origin of attendance updation requests. let secret_key = std::env::var("ROOT_SECRET").expect("ROOT_SECRET must be set."); + // DATABASE_URL provides the connection string for the PostgreSQL database. let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set."); // BIND_ADDRESS is used to determine the IP address for the server's socket to bind to. let bind_addr = std::env::var("BIND_ADDRESS").expect("BIND_ADDRESS must be set."); - if env == "development" { + if env == "production" { tracing_subscriber::registry() - .with(fmt::layer().pretty().with_writer(std::io::stdout)) + // In production, no need to write to stdout, write directly to file. .with( fmt::layer() .pretty() - .with_ansi(false) + .with_ansi(false) // ANSI encodings make it pretty but unreadable in the raw file. .with_writer(std::fs::File::create("root.log").unwrap()), ) - .with(EnvFilter::new("trace")) + // Allow only [`info`] and above events. + .with(EnvFilter::new("info")) .init(); - info!("Running in development mode."); + info!("Running in production mode.") } else { tracing_subscriber::registry() + // Write to both stdout and file in development. + .with(fmt::layer().pretty().with_writer(std::io::stdout)) .with( fmt::layer() .pretty() .with_ansi(false) .with_writer(std::fs::File::create("root.log").unwrap()), ) - .with(EnvFilter::new("info")) + // Allow all events. + .with(EnvFilter::new("trace")) .init(); - info!("Running in production mode.") + info!("Running in development mode."); } let pool = sqlx::postgres::PgPoolOptions::new() .min_connections(2) // Maintain at least two connections, one for amD and one for Home. It should be - .max_connections(3) // pretty unlikely that amD, Home and the web interface is used simultaneously + .max_connections(3) // pretty unlikely that amD, Home and the web interface is used simultaneously. .connect(&database_url) .await .expect("Pool must be initialized properly."); @@ -91,7 +98,7 @@ async fn main() { .await .expect("Failed to run migrations."); - // Wrap pool in an Arc to share across threads + // Wrap pool in an Arc to share across threads. let pool = Arc::new(pool); let schema = @@ -100,14 +107,16 @@ async fn main() { .data(secret_key) .finish(); - // This thread will sleep until it's time to run the daily task - // Also takes ownership of pool + // This thread will sleep until it's time to run the daily task. + // Also takes ownership of pool. tokio::task::spawn(async { run_daily_task_at_midnight(pool).await; }); let cors = CorsLayer::new() - .allow_origin(HeaderValue::from_static("https://home.amfoss.in")) // Only allow requests from Home + // Home should be the only website that accesses the API, bots and scripts do not trigger CORS AFAIK. + // This lets us restrict who has access to what in the API on the Home frontend. + .allow_origin(HeaderValue::from_static("https://home.amfoss.in")) .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) .allow_headers(tower_http::cors::Any); @@ -124,7 +133,7 @@ async fn main() { axum::serve(listener, router).await.unwrap(); } -/// Sleep till midnight, then run the 'execute_daily_task' function. +/// Continuously sleep till midnight, then run the 'execute_daily_task' function. async fn run_daily_task_at_midnight(pool: Arc) { loop { let now = chrono::Local::now().with_timezone(&Kolkata); From e2207f75970fda837f71e57e7ab0695e41713c65 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Fri, 17 Jan 2025 13:52:18 +0530 Subject: [PATCH 27/38] refactor main.rs to be more high-level by abstracing away its processes --- Cargo.toml | 2 +- src/daily_task/daily_task.rs | 21 ++++- src/daily_task/mod.rs | 2 +- src/graphql/mod.rs | 12 +++ src/main.rs | 151 ++++++++++++++++------------------- src/routes.rs | 32 +++++++- 6 files changed, 133 insertions(+), 87 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 12a6e0e..5f41d4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ axum = "0.7.3" chrono = "0.4.38" serde = { version = "1.0.188", features = ["derive"] } sqlx = { version = "0.7.1", features = ["chrono", "postgres", "runtime-tokio"] } -tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } # For async tests +tokio = { version = "1.28.2", features = ["default", "macros", "rt-multi-thread"] } # For async tests hmac = "0.12.1" sha2 = "0.10.8" hex = "0.4.3" diff --git a/src/daily_task/daily_task.rs b/src/daily_task/daily_task.rs index a49181b..d018d9f 100644 --- a/src/daily_task/daily_task.rs +++ b/src/daily_task/daily_task.rs @@ -1,15 +1,34 @@ use chrono::{Datelike, Local, NaiveDate, NaiveTime}; use chrono_tz::Asia::Kolkata; use sqlx::PgPool; +use tokio::time::sleep_until; use std::sync::Arc; use tracing::{debug, error}; use crate::models::member::Member; +/// Continuously sleep till midnight, then run the 'execute_daily_task' function. +pub async fn run_daily_task_at_midnight(pool: Arc) { + loop { + let now = chrono::Local::now().with_timezone(&Kolkata); + let next_midnight = (now + chrono::Duration::days(1)) + .date_naive() + .and_hms_opt(0, 0, 0) + .unwrap(); + + let duration_until_midnight = next_midnight.signed_duration_since(now.naive_local()); + let sleep_duration = + tokio::time::Duration::from_secs(duration_until_midnight.num_seconds() as u64); + + sleep_until(tokio::time::Instant::now() + sleep_duration).await; + execute_daily_task(pool.clone()).await; + } +} + /// This function does a number of things, including: /// * Insert new attendance records everyday for [`presense`](https://www.github.com/amfoss/presense) to update them later in the day. /// * Update the AttendanceSummary table -pub async fn execute_daily_task(pool: Arc) { +async fn execute_daily_task(pool: Arc) { // Members is queried outside of each function to avoid repetition let members = sqlx::query_as::<_, Member>("SELECT * FROM Member") .fetch_all(&*pool) diff --git a/src/daily_task/mod.rs b/src/daily_task/mod.rs index 993b199..b390f72 100644 --- a/src/daily_task/mod.rs +++ b/src/daily_task/mod.rs @@ -1,3 +1,3 @@ pub mod daily_task; -pub use daily_task::execute_daily_task; +pub use daily_task::run_daily_task_at_midnight; diff --git a/src/graphql/mod.rs b/src/graphql/mod.rs index c2baadf..f47dab5 100644 --- a/src/graphql/mod.rs +++ b/src/graphql/mod.rs @@ -1,2 +1,14 @@ +use async_graphql::MergedObject; +use mutations::{AttendanceMutations, MemberMutations, StreakMutations}; +use queries::{AttendanceQueries, MemberQueries, StreakQueries}; + pub mod mutations; pub mod queries; + +// This is our main query or QueryRoot. It is made up of structs representing sub-queries, one for each table in the DB. The fields of a relation are exposed via the [`async_graphql::SimpleObject`] directive on the [`models`] themselves. Specific queries, such as getting a member by ID or getting the streak of a member is defined as methods of the sub-query struct. Complex queries, such as those getting related data from multiple tables like querying all members and the streaks of each member, are defined via the [`async_graphql::ComplexObject`] directive on the [`models`] and can be found in the corresponding sub-query module. +#[derive(MergedObject, Default)] +pub struct Query(MemberQueries, AttendanceQueries, StreakQueries); + +// Mutations work the same as Queries, sub-modules for each relation in the DB. However, all methods are directly defined on these sub-module structs. But they use slightly modified versions of the [`models`], marked by the Input in the name, to get input. +#[derive(MergedObject, Default)] +pub struct Mutation(MemberMutations, AttendanceMutations, StreakMutations); diff --git a/src/main.rs b/src/main.rs index f1b0cc5..99cdad0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,14 @@ -use async_graphql::{EmptySubscription, MergedObject}; -use async_graphql_axum::GraphQL; -use axum::{ - http::{HeaderValue, Method}, - routing::get, - Router, -}; -use chrono_tz::Asia::Kolkata; +use async_graphql::EmptySubscription; +use axum::http::{HeaderValue, Method}; use sqlx::PgPool; use std::sync::Arc; -use tokio::time::sleep_until; use tower_http::cors::CorsLayer; use tracing::info; use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; -use daily_task::execute_daily_task; -use graphql::{ - mutations::{AttendanceMutations, MemberMutations, StreakMutations}, - queries::{AttendanceQueries, MemberQueries, StreakQueries}, -}; +use daily_task::run_daily_task_at_midnight; +use graphql::{Mutation, Query}; +use routes::setup_router; /// Daily task contains the function that is executed daily at midnight, using the thread spawned in main(). pub mod daily_task; @@ -28,35 +19,59 @@ pub mod models; /// Since we really only need one route for a GraphQL server, this just holds a function returning the GraphiQL playground. Probably can clean this up later. pub mod routes; -// This is our main query or QueryRoot. It is made up of structs representing sub-queries, one for each table in the DB. The fields of a relation are exposed via the [`async_graphql::SimpleObject`] directive on the [`models`] themselves. Specific queries, such as getting a member by ID or getting the streak of a member is defined as methods of the sub-query struct. Complex queries, such as those getting related data from multiple tables like querying all members and the streaks of each member, are defined via the [`async_graphql::ComplexObject`] directive on the [`models`] and can be found in the corresponding sub-query module. -#[derive(MergedObject, Default)] -struct Query(MemberQueries, AttendanceQueries, StreakQueries); +/// Handles all over environment variables in one place. +struct Config { + env: String, + secret_key: String, + database_url: String, + bind_address: String, +} -// Mutations work the same as Queries, sub-modules for each relation in the DB. However, all methods are directly defined on these sub-module structs. But they use slightly modified versions of the [`models`], marked by the Input in the name, to get input. -#[derive(MergedObject, Default)] -struct Mutations(MemberMutations, AttendanceMutations, StreakMutations); +impl Config { + fn from_env() -> Self { + // Currently, we need the DATABASE_URL to be loaded in through the .env. + // In the future, if we use any other configuration (say Github Secrets), we + // can allow dotenv() to err. + dotenv::dotenv().expect(".ENV file must be set up."); + Self { + // RUST_ENV is used to check if it's in production to avoid unnecessary logging and exposing the + // graphiql interface. Make sure to set it to "production" before deployment. + env: std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()), + // ROOT_SECRET is used to cryptographically verify the origin of attendance updation requests. + secret_key: std::env::var("ROOT_SECRET").expect("ROOT_SECRET must be set."), + // DATABASE_URL provides the connection string for the PostgreSQL database. + database_url: std::env::var("DATABASE_URL").expect("DATABASE_URL must be set."), + // BIND_ADDRESS is used to determine the IP address for the server's socket to bind to. + bind_address: std::env::var("BIND_ADDRESS").expect("BIND_ADDRESS must be set."), + } + } +} #[tokio::main] async fn main() { // 12/1/25: Going to assume this is only necessary for shuttle. // 9/1/25: TODO: Explain? // env::set_var("PGOPTIONS", "-c ignore_version=true"); + let config = Config::from_env(); + setup_tracing(&config.env); + + let pool = setup_database(&config.database_url).await; + let schema = build_graphql_schema(pool.clone(), config.secret_key); + + tokio::task::spawn(async { + run_daily_task_at_midnight(pool).await; + }); - // Currently, we need the DATABASE_URL to be loaded in through the .env. - // In the future, if we use any other configuration (say Github Secrets), we - // can allow dotenv() to err. - dotenv::dotenv().expect("Failed to load .env file."); + let cors = setup_cors(); + let router = setup_router(schema, cors, config.env == "development"); - // RUST_ENV is used to check if it's in production to avoid unnecessary logging and exposing the - // graphiql interface. Make sure to set it to "production" before deployment. - let env = std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()); - // ROOT_SECRET is used to cryptographically verify the origin of attendance updation requests. - let secret_key = std::env::var("ROOT_SECRET").expect("ROOT_SECRET must be set."); - // DATABASE_URL provides the connection string for the PostgreSQL database. - let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set."); - // BIND_ADDRESS is used to determine the IP address for the server's socket to bind to. - let bind_addr = std::env::var("BIND_ADDRESS").expect("BIND_ADDRESS must be set."); + info!("Starting Root..."); + let listener = tokio::net::TcpListener::bind(config.bind_address).await.unwrap(); + axum::serve(listener, router).await.unwrap(); +} +/// Abstraction over initializing the global subscriber for tracing depending on whether it's in production or dev. +fn setup_tracing(env: &str) { if env == "production" { tracing_subscriber::registry() // In production, no need to write to stdout, write directly to file. @@ -85,11 +100,14 @@ async fn main() { .init(); info!("Running in development mode."); } +} +/// Abstraction over setting up the database pool. +async fn setup_database(database_url: &str) -> Arc { let pool = sqlx::postgres::PgPoolOptions::new() - .min_connections(2) // Maintain at least two connections, one for amD and one for Home. It should be - .max_connections(3) // pretty unlikely that amD, Home and the web interface is used simultaneously. - .connect(&database_url) + .min_connections(2) + .max_connections(3) + .connect(database_url) .await .expect("Pool must be initialized properly."); @@ -98,55 +116,26 @@ async fn main() { .await .expect("Failed to run migrations."); - // Wrap pool in an Arc to share across threads. - let pool = Arc::new(pool); - - let schema = - async_graphql::Schema::build(Query::default(), Mutations::default(), EmptySubscription) - .data(pool.clone()) - .data(secret_key) - .finish(); + Arc::new(pool) +} - // This thread will sleep until it's time to run the daily task. - // Also takes ownership of pool. - tokio::task::spawn(async { - run_daily_task_at_midnight(pool).await; - }); +/// Abstraction over setting up the GraphQL schema from [`Query`] and [`Mutation`], and adding a reference to [`pool`] and [`secret_key`]. +fn build_graphql_schema( + pool: Arc, + secret_key: String, +) -> async_graphql::Schema { + async_graphql::Schema::build(Query::default(), Mutation::default(), EmptySubscription) + .data(pool) + .data(secret_key) + .finish() +} - let cors = CorsLayer::new() +/// Abstraction over making the CORSLayer. +fn setup_cors() -> CorsLayer { + CorsLayer::new() // Home should be the only website that accesses the API, bots and scripts do not trigger CORS AFAIK. // This lets us restrict who has access to what in the API on the Home frontend. .allow_origin(HeaderValue::from_static("https://home.amfoss.in")) .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) - .allow_headers(tower_http::cors::Any); - - info!("Starting Root..."); - // TODO: Avoid exposing the GraphiQL interface in prod. - let router = Router::new() - .route( - "/", - get(routes::graphiql).post_service(GraphQL::new(schema.clone())), - ) - .layer(cors); - - let listener = tokio::net::TcpListener::bind(bind_addr).await.unwrap(); - axum::serve(listener, router).await.unwrap(); -} - -/// Continuously sleep till midnight, then run the 'execute_daily_task' function. -async fn run_daily_task_at_midnight(pool: Arc) { - loop { - let now = chrono::Local::now().with_timezone(&Kolkata); - let next_midnight = (now + chrono::Duration::days(1)) - .date_naive() - .and_hms_opt(0, 0, 0) - .unwrap(); - - let duration_until_midnight = next_midnight.signed_duration_since(now.naive_local()); - let sleep_duration = - tokio::time::Duration::from_secs(duration_until_midnight.num_seconds() as u64); - - sleep_until(tokio::time::Instant::now() + sleep_duration).await; - execute_daily_task(pool.clone()).await; - } + .allow_headers(tower_http::cors::Any) } diff --git a/src/routes.rs b/src/routes.rs index 1ec4c07..877de46 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,9 +1,35 @@ -use async_graphql::http::GraphiQLSource; -use axum::response::{Html, IntoResponse}; +use async_graphql::{http::GraphiQLSource, EmptySubscription, Schema}; +use async_graphql_axum::GraphQL; +use axum::{ + response::{Html, IntoResponse}, + routing::get, + Router, +}; +use tower_http::cors::CorsLayer; + +use crate::graphql::{Mutation, Query}; + +/// Setups the router with the given Schema and CORSLayer. Additionally, adds the GraphiQL playground if `is_dev` is true. +pub fn setup_router( + schema: Schema, + cors: CorsLayer, + is_dev: bool, +) -> Router { + let mut router = Router::new() + .route_service("/graphql", GraphQL::new(schema.clone())) + .layer(cors); + + if is_dev { + // Add GraphiQL playground only in development mode + router = router.route("/", get(graphiql)); + } + + router +} // TODO: We do not want to expose GraphiQL unless in dev. /// Returns the built-in GraphQL playground from async_graphql. -pub async fn graphiql() -> impl IntoResponse { +async fn graphiql() -> impl IntoResponse { Html( GraphiQLSource::build() .endpoint("/") From 2d3c4975c2aace2332b5577b5bcd5565976b87cb Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Fri, 17 Jan 2025 14:05:37 +0530 Subject: [PATCH 28/38] rename endpoints for more clarity --- src/routes.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/routes.rs b/src/routes.rs index 877de46..b8832c2 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -15,24 +15,24 @@ pub fn setup_router( cors: CorsLayer, is_dev: bool, ) -> Router { - let mut router = Router::new() - .route_service("/graphql", GraphQL::new(schema.clone())) + let router = Router::new() + .route_service("/", GraphQL::new(schema)) .layer(cors); if is_dev { // Add GraphiQL playground only in development mode - router = router.route("/", get(graphiql)); + tracing::info!("GraphiQL playground enabled at /graphiql"); + router.route("/graphiql", get(graphiql)) + } else { + router } - - router } -// TODO: We do not want to expose GraphiQL unless in dev. /// Returns the built-in GraphQL playground from async_graphql. async fn graphiql() -> impl IntoResponse { Html( GraphiQLSource::build() - .endpoint("/") + .endpoint("/graphiql") .subscription_endpoint("/ws") .finish(), ) From 3be56a5fffa4a1ab5debac9adee5c2fb56ea0400 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Fri, 17 Jan 2025 14:12:27 +0530 Subject: [PATCH 29/38] complete todo to handle error when failing to fetch members --- src/daily_task/daily_task.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/daily_task/daily_task.rs b/src/daily_task/daily_task.rs index d018d9f..41c9e64 100644 --- a/src/daily_task/daily_task.rs +++ b/src/daily_task/daily_task.rs @@ -38,7 +38,7 @@ async fn execute_daily_task(pool: Arc) { // Add additional daily tasks such as leaderboard updates to the Ok(members) arm Ok(members) => update_attendance(members, &*pool).await, // TODO: Handle this - Err(_) => (), + Err(e) => error!("Failed to fetch members: {:?}", e), }; } From 3512e6cfbcdfc1a22d6fe885023d4ee949f199b4 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Fri, 17 Jan 2025 14:12:34 +0530 Subject: [PATCH 30/38] add documentation to models --- src/models/attendance.rs | 1 + src/models/member.rs | 1 + src/models/status_update_streak.rs | 2 ++ 3 files changed, 4 insertions(+) diff --git a/src/models/attendance.rs b/src/models/attendance.rs index 7faa7ed..a9d2268 100644 --- a/src/models/attendance.rs +++ b/src/models/attendance.rs @@ -41,6 +41,7 @@ pub struct AttendanceSummaryInfo { pub days_attended: i32, } +/// This struct is used to deserialize the input recieved for mutations on attendance. #[derive(InputObject)] pub struct MarkAttendanceInput { pub member_id: i32, diff --git a/src/models/member.rs b/src/models/member.rs index e70478c..af09cfb 100644 --- a/src/models/member.rs +++ b/src/models/member.rs @@ -27,6 +27,7 @@ pub struct Member { pub created_at: NaiveDateTime, } +/// This struct is used to deserialize the input recieved for mutations on Memeber. #[derive(InputObject)] pub struct CreateMemberInput { pub roll_no: String, diff --git a/src/models/status_update_streak.rs b/src/models/status_update_streak.rs index 4cb3451..1724e25 100644 --- a/src/models/status_update_streak.rs +++ b/src/models/status_update_streak.rs @@ -8,12 +8,14 @@ pub struct StatusUpdateStreak { pub max_streak: i32, } +/// This struct is used in place of [`StatusUpdateStreak`] in nested queries to avoid overfetching. #[derive(SimpleObject, FromRow)] pub struct StatusUpdateStreakInfo { pub current_streak: i32, pub max_streak: i32, } +/// This struct is used to deserialize the input recieved for mutations on StatusUpdateStreak. #[derive(InputObject)] pub struct IncrementStreakInput { pub member_id: Option, From 127cb5eab45d826c7ea2e42c5fe3f67c7979f93d Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Fri, 17 Jan 2025 17:02:02 +0530 Subject: [PATCH 31/38] fix graphiql missing graphql schema --- src/routes.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes.rs b/src/routes.rs index b8832c2..df90b67 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -16,13 +16,13 @@ pub fn setup_router( is_dev: bool, ) -> Router { let router = Router::new() - .route_service("/", GraphQL::new(schema)) + .route_service("/", GraphQL::new(schema.clone())) .layer(cors); if is_dev { // Add GraphiQL playground only in development mode tracing::info!("GraphiQL playground enabled at /graphiql"); - router.route("/graphiql", get(graphiql)) + router.route("/graphiql", get(graphiql).post_service(GraphQL::new(schema))) } else { router } From ffd6879b8892316a6d49724857f9bc4cc3c95bfb Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Fri, 17 Jan 2025 18:04:27 +0530 Subject: [PATCH 32/38] update documentation for root --- README.md | 82 +++++++++++++++--- docs/attendance.md | 71 ++++++++++++++++ docs/database.md | 77 +++++++++++++++++ docs/docs.md | 37 ++++++-- docs/member.md | 62 ++++++++++++++ docs/migrations.md | 40 --------- docs/mutations.md | 188 ---------------------------------------- docs/queries.md | 207 --------------------------------------------- docs/streaks.md | 43 ++++++++++ 9 files changed, 354 insertions(+), 453 deletions(-) create mode 100644 docs/attendance.md create mode 100644 docs/database.md create mode 100644 docs/member.md delete mode 100644 docs/migrations.md delete mode 100644 docs/mutations.md delete mode 100644 docs/queries.md create mode 100644 docs/streaks.md diff --git a/README.md b/README.md index 99bbc49..9d79ad2 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,80 @@

Root

-

a backend to manage all club information

+

A GraphQL backend for managing club member information

-### Overview -**Root** is a backend for managing all club related info; most other projects will be getting or publishing their data to root. +--- -### Documentation +Root is our club's backend, responsible for collecting and distributing data from and to all the other services including [Home](https://www.github.com/amfoss/home), [amD](https://www.github.com/amfoss/amd) and [Presense](https://www.github.com/amfoss/presense). The idea is to have all our data easily available in one place and to let every other end-user applications to be standalone. This ensures there's no single point of failure for all our infrastructure (as was the case with our previous CMS). Though Root going down would definitely cause a few features to stop working on the other apps. -Explore the [Documentation](/docs/docs.md) for detailed information and usage guidelines. +# Quick Setup ---- +1. Install prerequisites: + - Rust (latest stable should work fine) + - PostgreSQL + - SQLx CLI: `cargo install sqlx-cli` + +2. Configure environment: + ```bash + touch .env + ``` + +The following environment variables are required: +* DATABASE_URL: Connection string to your DB. +* RUST_ENV: Use "development" or "production" as applicable. +* ROOT_SECRET: Used to verify the origin of mutation requests on attendance. Ask the maintainers for it. +* BIND_ADDRESS: The IP address for `axum` to serve to. Typically `0.0.0.0:3000` for local deployments. + +3. Setup database: + ```bash + sqlx database create + sqlx migrate run + ``` + +4. Run server: + ```bash + cargo run + ``` + +GraphQL playground should be available at `http://localhost:8000/graphiql` as long as it's in development mode. + +# Documentation + +See the [documentation](docs/docs.md) for the API reference, database schema and other detailed documentation. + +# Contributing + +## Reporting Issues + +If you encounter a bug, please check existing issues first to avoid duplicates. If none exist, create a new issue with the following details: + +* Title: Concise summary. +* Description: A detailed description of the issue. +* Steps to Reproduce: If it's a bug, include steps to reproduce. +* Expected and Actual Behavior: Describe what you expected and what actually happened. + +## Suggesting Features + +We welcome new ideas! Please open an issue titled "Feature Request: ``" and provide: + +* Problem: What problem does this feature solve? +* Solution: Describe how you envision it working. +* Alternatives Considered: Mention any alternatives you've considered. + +## Submitting Code Changes + +If you'd like to fix a bug, add a feature, or improve code quality: + +* Check the open issues to avoid redundancy. +* Open a draft PR if you'd like feedback on an ongoing contribution. + +## Coding Standards -### How to Contribute +* Follow Rust Conventions: Use idiomatic Rust patterns. Use `cargo fmt` and `cargo clippy` to format and lint your code. +* Modularity: Write modular, reusable functions. Avoid monolithic code. +* Descriptive Naming: Use descriptive names for variables, functions, and types. +* Don't worry too much about rules, it just needs to be pretty. Most editors have built-in tools to do this for you. -1. Fork the repository and clone it to your local machine. -2. Set up the project by following the installation instructions above. -3. Identify an issue or feature you'd like to work on, and create an issue to track it. -4. Develop the patch or feature, ensuring it is thoroughly tested. -5. Submit a pull request, referencing the relevant issue number. +# License -### License This project is licensed under GNU General Public License V3. You are welcome to adapt it, make it yours. Just make sure that you credit us too. diff --git a/docs/attendance.md b/docs/attendance.md new file mode 100644 index 0000000..b895fe6 --- /dev/null +++ b/docs/attendance.md @@ -0,0 +1,71 @@ +# Attendance System + +Track daily member attendance and generate monthly attendance summaries. The summaries are used for quick access to see how many days a member has attended in a specific month, used in the amD attendance report. + +## Models + +### Attendance +```rust +struct Attendance { + attendance_id: i32, + member_id: i32, + date: NaiveDate, + is_present: bool, + time_in: Option, + time_out: Option, + created_at: NaiveDateTime, + updated_at: NaiveDateTime, +} +``` +The final two fields are not exposed in the interface for obvious reasons. + +### AttendanceSummary +Monthly attendance summary for each member. +```rust +struct AttendanceSummary { + member_id: i32, + year: i32, + month: i32, + days_attended: i32, +} +``` + +## Queries + +### Mark Attendance +Record a member's attendance for the day. + +```graphql +mutation { + markAttendance( + input: { + memberId: 1 + date: "2025-01-15" + timeIn: "09:00:00" + timeOut: "17:00:00" + } + ) { + attendanceId + isPresent + timeIn + timeOut + } +} +``` + +### Get Attendance Summary +Get monthly attendance summary for a member. + +```graphql +query { + attendanceSummary(memberId: 1) { + year + month + daysAttended + } +} +``` + +## Daily Task + +The `src/daily_task/daily_task.rs` system automatically updates attendance summaries at midnight. diff --git a/docs/database.md b/docs/database.md new file mode 100644 index 0000000..97ee0bc --- /dev/null +++ b/docs/database.md @@ -0,0 +1,77 @@ +# Database Migrations + +## Overview +Database schema is managed using SQLx migrations. Do not tamper with the database yourself (using clients such as psql) or with the migrations files. + +## Current Schema + +The entire schema, including constraints, is visible in `migrations/`. + +### Member Table +```sql +CREATE TABLE Member ( + member_id SERIAL PRIMARY KEY, + roll_no VARCHAR NOT NULL UNIQUE, + name VARCHAR NOT NULL, + email VARCHAR NOT NULL UNIQUE, + sex sex_type NOT NULL, + year INT NOT NULL, + hostel VARCHAR NOT NULL, + mac_address VARCHAR NOT NULL, + discord_id VARCHAR NOT NULL, + group_id INT NOT NULL, + created_at TIMESTAMP NOT NULL +); +``` + +### Attendance Table +```sql +CREATE TABLE Attendance ( + attendance_id SERIAL PRIMARY KEY, + member_id INT REFERENCES Member(member_id), + date DATE NOT NULL, + is_present BOOLEAN NOT NULL, + time_in TIME, + time_out TIME, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL +); +``` + +### AttendanceSummary Table +```sql +CREATE TABLE AttendanceSummary ( + member_id INT REFERENCES Member(member_id), + year INT NOT NULL, + month INT NOT NULL, + days_attended INT NOT NULL DEFAULT 0, + PRIMARY KEY (member_id, year, month) +); +``` + +### StatusUpdateStreak Table +```sql +CREATE TABLE StatusUpdateStreak ( + member_id INT REFERENCES Member(member_id), + current_streak INT NOT NULL DEFAULT 0, + max_streak INT NOT NULL, + PRIMARY KEY (member_id) +); +``` + +## Managing Migrations + +### Create Migration +```bash +sqlx migrate add +``` + +### Run Migrations +```bash +sqlx migrate run +``` + +### Revert Migration +```bash +sqlx migrate revert +``` diff --git a/docs/docs.md b/docs/docs.md index 39487c1..0c3b042 100644 --- a/docs/docs.md +++ b/docs/docs.md @@ -1,6 +1,33 @@ -# API Documentation +# Root Documentation -## Contents -- [Managing Migrations](/docs/migrations.md) -- [GraphQL Queries](/docs/queries.md) -- [GraphQL Mutations](/docs/mutations.md) \ No newline at end of file +## Project Structure +``` +src/ +├── graphql/ # GraphQL schema definitions +│ ├── mutations/ # Data modification operations +│ └── queries/ # Data retrieval operations +├── models/ # Database models and types +├── daily_task/ # Self explanatory +└── routes.rs # HTTP routing setup +``` + +## GraphQL API Structure +- [Member Management](member.md) - Managing club member profiles +- [Attendance System](attendance.md) - Daily attendance tracking and summaries +- [Status Streaks](streaks.md) - Tracking daily status update streaks + +## Database Schema +- [Database](database.md) - Database structure and migrations + +## Core Features +### Member Management +- Query members by ID, roll number, or Discord ID +- Create and update member profiles + +### Attendance System +- Mark daily attendance with time tracking +- Generate monthly attendance summaries + +### Status Updates +- Track daily status update streaks +- Record maximum streaks achieved diff --git a/docs/member.md b/docs/member.md new file mode 100644 index 0000000..091cf80 --- /dev/null +++ b/docs/member.md @@ -0,0 +1,62 @@ +# Member Management + +Manage club member profiles. This is the central entity for the database. Most things should relate to one or more members. + +## Models + +### Member +```rust +struct Member { + member_id: i32, + roll_no: String, + name: String, + email: String, + sex: Sex, + year: i32, + hostel: String, + mac_address: String, + discord_id: String, + group_id: i32, +} +``` + +## Queries + +### Get Member +Retrieve member details by ID, roll number, or Discord ID. + +```graphql +query { + getMember(rollNo: "AM.XX.U4XXX") { + name + email + year + } +} +``` + +## Mutations + +### Create Member +Add a new member to the database. + +```graphql +mutation { + createMember( + input: { + rollNo: "AM.XX.U4XXX" + name: "John Doe" + email: "john@amfoss.in" + sex: "M" + year: 2 + hostel: "MH" + macAddress: "XX:XX:XX:XX:XX:XX" + discordId: "123456789" + groupId: 1 + } + ) { + memberId + name + } +} +``` diff --git a/docs/migrations.md b/docs/migrations.md deleted file mode 100644 index 566bf20..0000000 --- a/docs/migrations.md +++ /dev/null @@ -1,40 +0,0 @@ -## Managing Migrations - -## Contents -- [Create Migrations](#create-migrations) -- [Run Migrations](#run-migrations) - ---- - -### Prerequisites -1. Install `sql-cli` - ```bash - cargo install sqlx-cli - ``` - -### Create Migrations - -Run the following command to create migrations - -```bash -sqlx migrate add -``` - -- Replace `` with a descriptive name for your migration, e.g.,`create_users_table`. -- This will add a new migration file to the `migrations` directory. - ---- - -### Run Migrations - -Run the following command to apply migrations to your local database - -```bash -sqlx migrate run -``` - ---- - -### Notes -- Look for logs confirming the migration ran successfully. -- Never rename or alter migration files as their names and contents are used to track applied changes. diff --git a/docs/mutations.md b/docs/mutations.md deleted file mode 100644 index 8e826da..0000000 --- a/docs/mutations.md +++ /dev/null @@ -1,188 +0,0 @@ -## GraphQL Mutations - -## Contents -- [addMember](#addmember) -- [editMember](#editmember) -- [markAttendance](#markattendance) -- [addAttendance](#addattendance) -- [setActiveProject](#setactiveproject) -- [removeActiveProject](#re) - ---- - -### addMember -Add a new member to the database. - -#### GraphQL Mutation -```graphql -mutation { - addMember(name: "name", email: "email", year: 2) { - id - name - year - } -} -``` - -#### Arguments (all required) -```graphql -rollno: String! -name: String! -hostel: String! -email: String! -sex: String! -year: Int! -macaddress: String! -discordId: String -``` - -**Note:** The `year` field represents the members's current college year -- `1` for first-year -- `2` for second-year -- `3` for third-year -- `4` for fourth-year - ---- - -### editMember -Edit details of an existing member. - -#### GraphQL Mutation -```graphql -mutation { - editMember(id:0,hostel:"hostel",year:2,macaddress:"mac_address",discordId:"discord_id",hmacSignature:"hmac_signature") { - id - hostel - discord_id - } -} -``` - -#### Arguments (all required) -```graphql -id: Int! -hostel: String! -year: Int! -macaddress: String! -discordId: String! -hmacSignature: String! -``` - -**Note:** Follow the below format if you want to leave some fields unchanged -- `''` (*empty string*) for type `String` - - Feilds: `hostel,macaddress,discordId` -- `0` for type `Int` - - Feilds: `year` - -For example, If you want to update only `discordId`: - -```graphql -mutation { - editMember(id:1,hostel:"",year:0,macaddress:"",discordId:"discord_id",hmacSignature:"hmac_signature") { - id - hostel - discord_id - } -} -``` - -Note: `id` and `hmacSignature` can't be empty - -#### HMAC Format - -``` -"{secret_key}{id}{hostel}{year}{macaddress}{discord_id}" -``` - ---- - -### markAttendance -Record attendance for a member. - -#### GraphQL Mutation -```graphql -mutation { - markAttendance(id: 0, date: "YYYY-MM-DD", isPresent: true, hmacSignature: "hmac_signature") { - id - date - isPresent - hmacSignature - } -} -``` - -#### Arguments (all required) -```graphql -id: Int! -date: NaiveDate! -isPresent: Boolean! -hmacSignature: String! -``` - ---- - -### addAttendance -Initiate attendance records for the day (*used internally*). - -#### GraphQL Mutation -```graphql -mutation { - addAttendance(id: 0, date: "YYYY-MM-DD", timein: "%H:%M:%S%.f", timeout: "%H:%M:%S%.f", isPresent:true) { - id - date - isPresent - hmacSignature - } -} -``` - -#### Arguments (all required) -```graphql -id: Int! -date: NaiveDate! -timein: NaiveTime! -timeout: NaiveTime! -isPresent: Boolean! -``` ---- - -### setActiveProject -Set active project for a member. - -#### GraphQL Mutation -```graphql -mutation { - setActiveProject(id:0,projectName:"project_name") { - id - memberId - projectTitle - } -} -``` - -#### Arguments (all required) -```graphql -id: Int! -projectName: String! -``` - ---- - -### removeActiveProject -Remove active project for a member. - -#### GraphQL Mutation -```graphql -mutation { - removeActiveProject(projectId:0) { - id - memberId - projectTitle - } -} -``` - -#### Arguments (all required) -```graphql -projectId: Int! -``` \ No newline at end of file diff --git a/docs/queries.md b/docs/queries.md deleted file mode 100644 index 5ab0d32..0000000 --- a/docs/queries.md +++ /dev/null @@ -1,207 +0,0 @@ -## GraphQL Queries - -## Contents -- [getMember](#getmember) -- [getAttendance](#getattendance) -- [getAttendanceStreak](#getattendancestreak) -- [getAttendanceSummary](#getattendancesummary) -- [getNonWorkingDays](#getnonworkingdays) -- [getProjects](#getprojects) -- [getUpdateStreak](#getupdatestreak) - ---- - -### getMember -Retrieve all the members from the database. - -#### GraphQL Query -```graphql -query { - getMember { - id - name - email - } -} -``` - -#### Fields -```graphql -id: Int! -rollno: String! -name: String! -hostel: String! -email: String! -sex: String! -year: Int! -macaddress: String! -discordId: String! -``` - ---- - -### getAttendance -Retrieve attendance records for a specific date. - -#### GraphQL Query -```graphql -query { - getAttendance(date: "YYYY-MM-DD") { - id - date - timein - timeout - isPresent - } -} -``` - -#### Arguments -- `date` (required): A date in the format `YYYY-MM-DD`. - -#### Fields -```graphql -id: Int! -date: NaiveDate! -timein: NaiveTime! -timeout: NaiveTime! -isPresent: Boolean! -``` - ---- - -### getAttendanceStreak -Retrieve attendance streak between date ranges. - -#### GraphQL Query -```graphql -query { - getAttendanceStreak(startDate:"YYYY-MM-DD",endDate:"YYYY-MM-DD"){ - id - memberId - month - streak - } -} -``` - -#### Arguments -- `startDate` (required): A date in the format `YYYY-MM-DD`. -- `endDate` (required): A date in the format `YYYY-MM-DD`. - -#### Fields -```graphql -id: Int! -memberId: Int! -month: NaiveDate! -streak: Int! -``` - ---- - -### getAttendanceSummary -Retrieve attendance summary between date ranges. - -#### GraphQL Query -```graphql -query { - getAttendanceSummary(startDate:"YYYY-MM-DD",endDate:"YYYY-MM-DD") { - maxDays - memberAttendance { - id - presentDays - } - dailyCount { - date - count - } - } -} -``` - -#### Arguments -- `startDate` (required): A date in the format `YYYY-MM-DD`. -- `endDate` (required): A date in the format `YYYY-MM-DD`. - -#### Fields -Type: AttendanceSummary! -```graphql -maxDays: Int! -memberAttendance: [MemberAttendance!]! -dailyCount: [DailyCount!]! -``` - -Type: MemberAttendance! -```graphql -id: Int! -presentDays: Int! -``` - -Type: DailyCount! -```graphql -date: NaiveDate! -count: Int! -``` - ---- - -### getNonWorkingDays -Retrieve Non Working Days from root. - -#### GraphQL Query -```graphql -query { - getNonWorkingDays -} -``` - -#### Fields -```graphql -[NaiveDate!]! -``` - ---- - -### getProjects -Retrieve active project details for all members. - -#### GraphQL Query -```graphql -query { - getProjects { - id - memberId - projectTitle - } -} -``` - -#### Fields -```graphql -id: Int! -memberId: Int! -projectTitle: String -``` - ---- - -### getUpdateStreak -Retrieve Update streaks for all members. - -#### GraphQL Query -```graphql -query { - getUpdateStreak { - id - streak - maxStreak - } -} -``` - -#### Fields -```graphql -id: Int! -streak: Int -maxStreak: Int -``` \ No newline at end of file diff --git a/docs/streaks.md b/docs/streaks.md new file mode 100644 index 0000000..b988469 --- /dev/null +++ b/docs/streaks.md @@ -0,0 +1,43 @@ +# Status Update Streaks + +## Overview +Track members' daily status update streaks and records. + +## Models + +### StatusUpdateStreak +```rust +struct StatusUpdateStreak { + member_id: i32, + current_streak: i32, + max_streak: i32, +} +``` + +## Queries + +### Get Streak +```graphql +query { + getUpdateStreak(memberId: 1) { + currentStreak + maxStreak + } +} +``` + +## Mutations + +### Increment Streak +```graphql +mutation { + incrementStreak( + input: { + memberId: 1 + } + ) { + currentStreak + maxStreak + } +} +``` \ No newline at end of file From 173b39c0206cd7ad48d067e1fdd55e07a8231800 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Fri, 17 Jan 2025 18:10:23 +0530 Subject: [PATCH 33/38] add reset streak mutation --- src/daily_task/daily_task.rs | 2 +- src/graphql/mutations/streak_mutations.rs | 40 +++++++++++++++++++---- src/main.rs | 4 ++- src/models/status_update_streak.rs | 2 +- src/routes.rs | 5 ++- 5 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/daily_task/daily_task.rs b/src/daily_task/daily_task.rs index 41c9e64..404cb4d 100644 --- a/src/daily_task/daily_task.rs +++ b/src/daily_task/daily_task.rs @@ -1,8 +1,8 @@ use chrono::{Datelike, Local, NaiveDate, NaiveTime}; use chrono_tz::Asia::Kolkata; use sqlx::PgPool; -use tokio::time::sleep_until; use std::sync::Arc; +use tokio::time::sleep_until; use tracing::{debug, error}; use crate::models::member::Member; diff --git a/src/graphql/mutations/streak_mutations.rs b/src/graphql/mutations/streak_mutations.rs index a4b9510..8b2728b 100644 --- a/src/graphql/mutations/streak_mutations.rs +++ b/src/graphql/mutations/streak_mutations.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use async_graphql::{Context, Error, Object, Result}; use sqlx::PgPool; -use crate::models::status_update_streak::{IncrementStreakInput, StatusUpdateStreak as Streak}; +use crate::models::status_update_streak::{StatusUpdateStreak as Streak, StreakInput}; #[derive(Default)] pub struct StreakMutations; @@ -11,11 +11,7 @@ pub struct StreakMutations; #[Object] impl StreakMutations { #[graphql(name = "incrementStreak")] - async fn increment_streak( - &self, - ctx: &Context<'_>, - input: IncrementStreakInput, - ) -> Result { + async fn increment_streak(&self, ctx: &Context<'_>, input: StreakInput) -> Result { let pool = ctx.data::>().expect("Pool must be in context."); // Ensure at least one identifier is provided @@ -46,4 +42,36 @@ impl StreakMutations { Ok(updated_streak) } + + async fn reset_streak(&self, ctx: &Context<'_>, input: StreakInput) -> Result { + let pool = ctx.data::>().expect("Pool must be in context."); + + // Ensure at least one identifier is provided + if input.member_id.is_none() && input.discord_id.is_none() { + return Err(Error::new( + "Either `member_id` or `discord_id` must be provided.", + )); + } + + let mut sql = String::from("UPDATE Streaks SET current_streak = 0 WHERE "); + + if let Some(_) = input.member_id { + sql.push_str("member_id = $1"); + } else if let Some(_) = input.discord_id { + sql.push_str("discord_id = $1"); + } + + sql.push_str(" RETURNING *"); + + let query = if let Some(member_id) = input.member_id { + sqlx::query_as::<_, Streak>(&sql).bind(member_id) + } else if let Some(discord_id) = input.discord_id { + sqlx::query_as::<_, Streak>(&sql).bind(discord_id) + } else { + return Err(Error::new("Invalid input.")); + }; + + let updated_streak = query.fetch_one(pool.as_ref()).await?; + Ok(updated_streak) + } } diff --git a/src/main.rs b/src/main.rs index 99cdad0..67d60a3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -66,7 +66,9 @@ async fn main() { let router = setup_router(schema, cors, config.env == "development"); info!("Starting Root..."); - let listener = tokio::net::TcpListener::bind(config.bind_address).await.unwrap(); + let listener = tokio::net::TcpListener::bind(config.bind_address) + .await + .unwrap(); axum::serve(listener, router).await.unwrap(); } diff --git a/src/models/status_update_streak.rs b/src/models/status_update_streak.rs index 1724e25..5d26f84 100644 --- a/src/models/status_update_streak.rs +++ b/src/models/status_update_streak.rs @@ -17,7 +17,7 @@ pub struct StatusUpdateStreakInfo { /// This struct is used to deserialize the input recieved for mutations on StatusUpdateStreak. #[derive(InputObject)] -pub struct IncrementStreakInput { +pub struct StreakInput { pub member_id: Option, pub discord_id: Option, } diff --git a/src/routes.rs b/src/routes.rs index df90b67..1b445fb 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -22,7 +22,10 @@ pub fn setup_router( if is_dev { // Add GraphiQL playground only in development mode tracing::info!("GraphiQL playground enabled at /graphiql"); - router.route("/graphiql", get(graphiql).post_service(GraphQL::new(schema))) + router.route( + "/graphiql", + get(graphiql).post_service(GraphQL::new(schema)), + ) } else { router } From 7a804dc1710b5572d893e0ae02e657ac8e8ee808 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Fri, 17 Jan 2025 18:31:47 +0530 Subject: [PATCH 34/38] add hmac verification to mark attendance --- src/graphql/mutations/attendance_mutations.rs | 17 +++++++++++++++++ src/models/attendance.rs | 5 +++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/graphql/mutations/attendance_mutations.rs b/src/graphql/mutations/attendance_mutations.rs index aabfeb4..de087d9 100644 --- a/src/graphql/mutations/attendance_mutations.rs +++ b/src/graphql/mutations/attendance_mutations.rs @@ -4,9 +4,13 @@ use async_graphql::{Context, Object, Result}; use chrono::Local; use chrono_tz::Asia::Kolkata; use sqlx::PgPool; +use hmac::{Hmac,Mac}; +use sha2::Sha256; use crate::models::attendance::{Attendance, MarkAttendanceInput}; +type HmacSha256 = Hmac; + #[derive(Default)] pub struct AttendanceMutations; @@ -22,6 +26,19 @@ impl AttendanceMutations { .data::>() .expect("Pool not found in context"); + let secret_key = ctx.data::().expect("ROOT_SECRET must be found in context"); + + let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes()).expect("HMAC can take key of any size"); + let message = format!("{}{}{}{}", input.member_id, input.date, input.time_in, input.time_out); + mac.update(message.as_bytes()); + + let expected_signature = mac.finalize().into_bytes(); + let received_signature = hex::decode(input.hmac_signature)?; + + if expected_signature.as_slice() != received_signature.as_slice() { + return Err(async_graphql::Error::new("HMAC verification failed")); + } + let now = Local::now().with_timezone(&Kolkata).date_naive(); let attendance = sqlx::query_as::<_, Attendance>( "INSERT INTO Attendance (member_id, date, is_present, time_in, time_out, created_at, updated_at) diff --git a/src/models/attendance.rs b/src/models/attendance.rs index a9d2268..ebf15fb 100644 --- a/src/models/attendance.rs +++ b/src/models/attendance.rs @@ -46,6 +46,7 @@ pub struct AttendanceSummaryInfo { pub struct MarkAttendanceInput { pub member_id: i32, pub date: NaiveDate, - pub time_in: Option, - pub time_out: Option, + pub time_in: NaiveTime, + pub time_out: NaiveTime, + pub hmac_signature: String, } From 86904952e1a9ff92ccbe7af49e82f4aeebef8b92 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Sat, 18 Jan 2025 00:13:46 +0530 Subject: [PATCH 35/38] remove shuttle workaround in main.rs --- src/main.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 67d60a3..e07b900 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,9 +49,6 @@ impl Config { #[tokio::main] async fn main() { - // 12/1/25: Going to assume this is only necessary for shuttle. - // 9/1/25: TODO: Explain? - // env::set_var("PGOPTIONS", "-c ignore_version=true"); let config = Config::from_env(); setup_tracing(&config.env); From 7655247ea34c66d38b25eebd50a1e5dbb4ffed9a Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Fri, 24 Jan 2025 14:33:32 +0530 Subject: [PATCH 36/38] fix incorrect query in mark attendance and remove time fields from its input --- src/graphql/mutations/attendance_mutations.rs | 15 ++++++++------- src/models/attendance.rs | 2 -- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/graphql/mutations/attendance_mutations.rs b/src/graphql/mutations/attendance_mutations.rs index de087d9..7781851 100644 --- a/src/graphql/mutations/attendance_mutations.rs +++ b/src/graphql/mutations/attendance_mutations.rs @@ -29,7 +29,7 @@ impl AttendanceMutations { let secret_key = ctx.data::().expect("ROOT_SECRET must be found in context"); let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes()).expect("HMAC can take key of any size"); - let message = format!("{}{}{}{}", input.member_id, input.date, input.time_in, input.time_out); + let message = format!("{}{}", input.member_id, input.date); mac.update(message.as_bytes()); let expected_signature = mac.finalize().into_bytes(); @@ -41,15 +41,16 @@ impl AttendanceMutations { let now = Local::now().with_timezone(&Kolkata).date_naive(); let attendance = sqlx::query_as::<_, Attendance>( - "INSERT INTO Attendance (member_id, date, is_present, time_in, time_out, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $6) RETURNING *", + "UPDATE Attendance SET time_in = CASE + WHEN time_in IN NULL THEN $1 + ELSE time_in END, + time_out = $1 + WHERE id = $2 AND date = $3 RETURNING * + ", ) + .bind(now) .bind(input.member_id) .bind(input.date) - .bind(true) - .bind(input.time_in) - .bind(input.time_out) - .bind(now) .fetch_one(pool.as_ref()) .await?; diff --git a/src/models/attendance.rs b/src/models/attendance.rs index ebf15fb..d9a3668 100644 --- a/src/models/attendance.rs +++ b/src/models/attendance.rs @@ -46,7 +46,5 @@ pub struct AttendanceSummaryInfo { pub struct MarkAttendanceInput { pub member_id: i32, pub date: NaiveDate, - pub time_in: NaiveTime, - pub time_out: NaiveTime, pub hmac_signature: String, } From 356e5089d1f0b5056b331fb92f79cbf57fcdabca Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Fri, 24 Jan 2025 14:33:49 +0530 Subject: [PATCH 37/38] run cargo fmt --- src/graphql/mutations/attendance_mutations.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/graphql/mutations/attendance_mutations.rs b/src/graphql/mutations/attendance_mutations.rs index 7781851..1d8d7b1 100644 --- a/src/graphql/mutations/attendance_mutations.rs +++ b/src/graphql/mutations/attendance_mutations.rs @@ -3,9 +3,9 @@ use std::sync::Arc; use async_graphql::{Context, Object, Result}; use chrono::Local; use chrono_tz::Asia::Kolkata; -use sqlx::PgPool; -use hmac::{Hmac,Mac}; +use hmac::{Hmac, Mac}; use sha2::Sha256; +use sqlx::PgPool; use crate::models::attendance::{Attendance, MarkAttendanceInput}; @@ -26,9 +26,12 @@ impl AttendanceMutations { .data::>() .expect("Pool not found in context"); - let secret_key = ctx.data::().expect("ROOT_SECRET must be found in context"); + let secret_key = ctx + .data::() + .expect("ROOT_SECRET must be found in context"); - let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes()).expect("HMAC can take key of any size"); + let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes()) + .expect("HMAC can take key of any size"); let message = format!("{}{}", input.member_id, input.date); mac.update(message.as_bytes()); @@ -41,8 +44,8 @@ impl AttendanceMutations { let now = Local::now().with_timezone(&Kolkata).date_naive(); let attendance = sqlx::query_as::<_, Attendance>( - "UPDATE Attendance SET time_in = CASE - WHEN time_in IN NULL THEN $1 + "UPDATE Attendance SET time_in = CASE + WHEN time_in IN NULL THEN $1 ELSE time_in END, time_out = $1 WHERE id = $2 AND date = $3 RETURNING * From b23470018afee85124b5eab74ea34c1f121edaec Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Fri, 24 Jan 2025 16:21:30 +0530 Subject: [PATCH 38/38] add projects feature --- .../20250124095543_create_project_table.sql | 6 ++++ src/graphql/mod.rs | 18 +++++++++--- src/graphql/mutations/mod.rs | 2 ++ src/graphql/mutations/project_mutations.rs | 29 +++++++++++++++++++ src/graphql/queries/mod.rs | 2 ++ src/graphql/queries/project_queries.rs | 23 +++++++++++++++ src/models/mod.rs | 1 + src/models/project.rs | 15 ++++++++++ 8 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 migrations/20250124095543_create_project_table.sql create mode 100644 src/graphql/mutations/project_mutations.rs create mode 100644 src/graphql/queries/project_queries.rs create mode 100644 src/models/project.rs diff --git a/migrations/20250124095543_create_project_table.sql b/migrations/20250124095543_create_project_table.sql new file mode 100644 index 0000000..7df4103 --- /dev/null +++ b/migrations/20250124095543_create_project_table.sql @@ -0,0 +1,6 @@ +CREATE TABLE Project ( + project_id SERIAL PRIMARY KEY, + member_id INT NOT NULL, + title TEXT, + CONSTRAINT fkey_member FOREIGN KEY (member_id) REFERENCES Member(member_id) ON DELETE CASCADE +); diff --git a/src/graphql/mod.rs b/src/graphql/mod.rs index f47dab5..6614c9a 100644 --- a/src/graphql/mod.rs +++ b/src/graphql/mod.rs @@ -1,14 +1,24 @@ use async_graphql::MergedObject; -use mutations::{AttendanceMutations, MemberMutations, StreakMutations}; -use queries::{AttendanceQueries, MemberQueries, StreakQueries}; +use mutations::{AttendanceMutations, MemberMutations, ProjectMutations, StreakMutations}; +use queries::{AttendanceQueries, MemberQueries, ProjectQueries, StreakQueries}; pub mod mutations; pub mod queries; // This is our main query or QueryRoot. It is made up of structs representing sub-queries, one for each table in the DB. The fields of a relation are exposed via the [`async_graphql::SimpleObject`] directive on the [`models`] themselves. Specific queries, such as getting a member by ID or getting the streak of a member is defined as methods of the sub-query struct. Complex queries, such as those getting related data from multiple tables like querying all members and the streaks of each member, are defined via the [`async_graphql::ComplexObject`] directive on the [`models`] and can be found in the corresponding sub-query module. #[derive(MergedObject, Default)] -pub struct Query(MemberQueries, AttendanceQueries, StreakQueries); +pub struct Query( + MemberQueries, + AttendanceQueries, + StreakQueries, + ProjectQueries, +); // Mutations work the same as Queries, sub-modules for each relation in the DB. However, all methods are directly defined on these sub-module structs. But they use slightly modified versions of the [`models`], marked by the Input in the name, to get input. #[derive(MergedObject, Default)] -pub struct Mutation(MemberMutations, AttendanceMutations, StreakMutations); +pub struct Mutation( + MemberMutations, + AttendanceMutations, + StreakMutations, + ProjectMutations, +); diff --git a/src/graphql/mutations/mod.rs b/src/graphql/mutations/mod.rs index 3fa4c30..012ed2a 100644 --- a/src/graphql/mutations/mod.rs +++ b/src/graphql/mutations/mod.rs @@ -1,7 +1,9 @@ pub mod attendance_mutations; pub mod member_mutations; +pub mod project_mutations; pub mod streak_mutations; pub use attendance_mutations::AttendanceMutations; pub use member_mutations::MemberMutations; +pub use project_mutations::ProjectMutations; pub use streak_mutations::StreakMutations; diff --git a/src/graphql/mutations/project_mutations.rs b/src/graphql/mutations/project_mutations.rs new file mode 100644 index 0000000..4796efd --- /dev/null +++ b/src/graphql/mutations/project_mutations.rs @@ -0,0 +1,29 @@ +use std::sync::Arc; + +use async_graphql::{Context, Error, Object, Result}; +use sqlx::PgPool; + +use crate::models::project::{Project, SetProjectInput}; + +#[derive(Default)] +pub struct ProjectMutations; + +#[Object] +impl ProjectMutations { + #[graphql(name = "setProject")] + // input is member_id + async fn set_project(&self, ctx: &Context<'_>, input: SetProjectInput) -> Result { + let pool = ctx + .data::>() + .expect("Pool must be found in context"); + + let project = sqlx::query_as::<_, Project>( + "INSERT INTO Project (member_id, project_title) VALUES ($1, $2) RETURNING * ", + ) + .bind(input.member_id) + .bind(input.title) + .fetch_one(pool.as_ref()) + .await?; + Ok(project) + } +} diff --git a/src/graphql/queries/mod.rs b/src/graphql/queries/mod.rs index 1f416a4..49a8263 100644 --- a/src/graphql/queries/mod.rs +++ b/src/graphql/queries/mod.rs @@ -1,7 +1,9 @@ pub mod attendance_queries; pub mod member_queries; +pub mod project_queries; pub mod streak_queries; pub use attendance_queries::AttendanceQueries; pub use member_queries::MemberQueries; +pub use project_queries::ProjectQueries; pub use streak_queries::StreakQueries; diff --git a/src/graphql/queries/project_queries.rs b/src/graphql/queries/project_queries.rs new file mode 100644 index 0000000..3054d93 --- /dev/null +++ b/src/graphql/queries/project_queries.rs @@ -0,0 +1,23 @@ +use std::sync::Arc; + +use crate::models::project::Project; +use async_graphql::{Context, Object, Result}; +use sqlx::PgPool; + +/// Sub-query for the [`Project`] table. The queries are: +/// * projects - get all projects +#[derive(Default)] +pub struct ProjectQueries; + +#[Object] +impl ProjectQueries { + pub async fn projects(&self, ctx: &Context<'_>) -> Result> { + let pool = ctx.data::>().expect("Pool must be in context."); + + let projects = sqlx::query_as::<_, Project>("SELECT * FROM Project") + .fetch_all(pool.as_ref()) + .await?; + + Ok(projects) + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index e23fcc7..fbc7dac 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,3 +1,4 @@ pub mod attendance; pub mod member; +pub mod project; pub mod status_update_streak; diff --git a/src/models/project.rs b/src/models/project.rs new file mode 100644 index 0000000..8abdc50 --- /dev/null +++ b/src/models/project.rs @@ -0,0 +1,15 @@ +use async_graphql::{InputObject, SimpleObject}; +use sqlx::FromRow; + +#[derive(FromRow, SimpleObject)] +pub struct Project { + pub project_id: i32, + pub member_id: i32, + pub title: Option, +} + +#[derive(InputObject)] +pub struct SetProjectInput { + pub member_id: i32, + pub title: String, +}