From 0e034fe2995cc998b805eb6ebb99bf1fc47c6010 Mon Sep 17 00:00:00 2001 From: Aadarsh Mahesh K Date: Wed, 12 Mar 2025 18:31:09 +0530 Subject: [PATCH 01/10] feat: graphql schema for leaderboard --- .../20250312124630_add_leaderboard_tables.sql | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 migrations/20250312124630_add_leaderboard_tables.sql diff --git a/migrations/20250312124630_add_leaderboard_tables.sql b/migrations/20250312124630_add_leaderboard_tables.sql new file mode 100644 index 0000000..544e97f --- /dev/null +++ b/migrations/20250312124630_add_leaderboard_tables.sql @@ -0,0 +1,38 @@ +-- Add migration script here + +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(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(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(member_id) +); + +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); \ No newline at end of file From 67e13623d1ba52db07d1da54540f7a6097c77b8a Mon Sep 17 00:00:00 2001 From: Aadarsh Mahesh K Date: Wed, 12 Mar 2025 18:39:56 +0530 Subject: [PATCH 02/10] feat: add rust struct for leaderboard --- src/models/leaderboard.rs | 73 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/models/leaderboard.rs diff --git a/src/models/leaderboard.rs b/src/models/leaderboard.rs new file mode 100644 index 0000000..67afd6c --- /dev/null +++ b/src/models/leaderboard.rs @@ -0,0 +1,73 @@ +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, +} \ No newline at end of file From 3ca759b4bcc1281d40e3c6e6b73a2778dbfc4b76 Mon Sep 17 00:00:00 2001 From: Aadarsh Mahesh K Date: Fri, 14 Mar 2025 21:07:13 +0530 Subject: [PATCH 03/10] feat: add queries required for cp leaderboard --- src/graphql/queries/codeforces_status.rs | 53 ++++++++++ src/graphql/queries/leaderboard_queries.rs | 64 ++++++++++++ src/graphql/queries/leetcode_status.rs | 112 +++++++++++++++++++++ 3 files changed, 229 insertions(+) create mode 100644 src/graphql/queries/codeforces_status.rs create mode 100644 src/graphql/queries/leaderboard_queries.rs create mode 100644 src/graphql/queries/leetcode_status.rs diff --git a/src/graphql/queries/codeforces_status.rs b/src/graphql/queries/codeforces_status.rs new file mode 100644 index 0000000..726a815 --- /dev/null +++ b/src/graphql/queries/codeforces_status.rs @@ -0,0 +1,53 @@ +use serde_json::Value; +use sqlx::PgPool; +use std::sync::Arc; + +pub async fn fetch_codeforces_stats( + pool: Arc, + member_id: i32, + username: &str, +) -> Result<(), Box> { + let url = format!("https://codeforces.com/api/user.info?handles={}", username); + let response = reqwest::get(&url).await?; + let data: Value = response.json().await?; + + if let Some(user) = data["result"].as_array().and_then(|arr| arr.first()) { + let rating = user["rating"].as_i64().unwrap_or(0) as i32; + let max_rating = user["maxRating"].as_i64().unwrap_or(0) as i32; + let rank = user["rank"].as_str().unwrap_or("Unrated").to_string(); + let max_rank = user["maxRank"].as_str().unwrap_or("Unrated").to_string(); + + let update_result = sqlx::query( + " + INSERT INTO codeforces_stats ( + member_id, codeforces_username, rating, max_rating, rank, max_rank + ) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (member_id) DO UPDATE SET + codeforces_username = EXCLUDED.codeforces_username, + rating = EXCLUDED.rating, + max_rating = EXCLUDED.max_rating, + rank = EXCLUDED.rank, + max_rank = EXCLUDED.max_rank + " + ) + .bind(member_id) + .bind(username) + .bind(rating) + .bind(max_rating) + .bind(rank) + .bind(max_rank) + .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 + ), + } + } + + Ok(()) +} diff --git a/src/graphql/queries/leaderboard_queries.rs b/src/graphql/queries/leaderboard_queries.rs new file mode 100644 index 0000000..32c94a1 --- /dev/null +++ b/src/graphql/queries/leaderboard_queries.rs @@ -0,0 +1,64 @@ +use async_graphql::{Context, Object}; +use sqlx::PgPool; +use std::sync::Arc; + +use crate::db::leaderboard::{CodeforcesStatsWithName, LeaderboardWithMember, LeetCodeStatsWithName}; + +pub struct QueryRoot; + +#[Object] +impl QueryRoot { + 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::>() + .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) + } +} diff --git a/src/graphql/queries/leetcode_status.rs b/src/graphql/queries/leetcode_status.rs new file mode 100644 index 0000000..d33b2c4 --- /dev/null +++ b/src/graphql/queries/leetcode_status.rs @@ -0,0 +1,112 @@ +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 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(contests_participated) + .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(()) +} \ No newline at end of file From 6d1bd582fc18f494d523284af7acc0668f63709f Mon Sep 17 00:00:00 2001 From: Aadarsh Mahesh K Date: Fri, 14 Mar 2025 21:08:10 +0530 Subject: [PATCH 04/10] feat: add mutations required for cp leaderboard --- src/graphql/mutations/codeforces_status.rs | 77 ++++++++++++ src/graphql/mutations/leaderboard_mutation.rs | 56 +++++++++ src/graphql/mutations/leetcode_status.rs | 118 ++++++++++++++++++ src/graphql/mutations/mod.rs | 11 ++ src/graphql/mutations/update_leaderboard.rs | 113 +++++++++++++++++ 5 files changed, 375 insertions(+) create mode 100644 src/graphql/mutations/codeforces_status.rs create mode 100644 src/graphql/mutations/leaderboard_mutation.rs create mode 100644 src/graphql/mutations/leetcode_status.rs create mode 100644 src/graphql/mutations/update_leaderboard.rs diff --git a/src/graphql/mutations/codeforces_status.rs b/src/graphql/mutations/codeforces_status.rs new file mode 100644 index 0000000..8167966 --- /dev/null +++ b/src/graphql/mutations/codeforces_status.rs @@ -0,0 +1,77 @@ +use async_graphql::{Context, Object, Result}; +use reqwest; +use serde_json::Value; +use sqlx::PgPool; +use std::sync::Arc; + +#[derive(Default)] +pub struct CodeforcesStats; + +#[Object] +impl CodeforcesStats { + async fn fetch_codeforces_stats( + &self, + ctx: &Context<'_>, // Retrieve the database pool from context + member_id: i32, + username: String, + ) -> Result { + let pool = ctx.data::>()?; // Get the PgPool from context + + 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; + + 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 + "#, + member_id, + username, + codeforces_rating, + max_rating, + contests_participated + ) + .execute(pool.as_ref()) + .await; + + match update_result { + Ok(_) => Ok(format!( + "Codeforces stats updated successfully for member ID: {}", + member_id + )), + Err(e) => Err(format!( + "Failed to update Codeforces stats for member ID {}: {:?}", + member_id, e + ) + .into()), + } + } else { + Err("Invalid response from Codeforces API".into()) + } + } else { + Err("Codeforces API returned an error".into()) + } + } +} diff --git a/src/graphql/mutations/leaderboard_mutation.rs b/src/graphql/mutations/leaderboard_mutation.rs new file mode 100644 index 0000000..27c5590 --- /dev/null +++ b/src/graphql/mutations/leaderboard_mutation.rs @@ -0,0 +1,56 @@ +use async_graphql::{Context, Object}; +use sqlx::PgPool; +use std::sync::Arc; + +use crate::db::leaderboard::{CodeforcesStats, LeetCodeStats}; + +pub struct LeadMutation; + +#[Object] +impl LeadMutation { + async fn add_or_update_leetcode_username( + &self, + ctx: &Context<'_>, + member_id: i32, + username: String, + ) -> Result { + let pool = ctx.data::>()?; + + 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 = EXCLUDED.leetcode_username + RETURNING * + ", + ) + .bind(member_id) + .bind(username) + .fetch_one(pool.as_ref()) + .await + } + + async fn add_or_update_codeforces_handle( + &self, + ctx: &Context<'_>, + member_id: i32, + handle: String, + ) -> Result { + let pool = ctx.data::>()?; + + 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 = EXCLUDED.codeforces_handle + RETURNING * + ", + ) + .bind(member_id) + .bind(handle) + .fetch_one(pool.as_ref()) + .await + } +} diff --git a/src/graphql/mutations/leetcode_status.rs b/src/graphql/mutations/leetcode_status.rs new file mode 100644 index 0000000..ec983b2 --- /dev/null +++ b/src/graphql/mutations/leetcode_status.rs @@ -0,0 +1,118 @@ +use async_graphql::{Context, Object, Result}; +use reqwest::Client; +use serde_json::Value; +use sqlx::PgPool; +use std::sync::Arc; + +#[derive(Default)] +pub struct LeetCodeStats; + +#[Object] +impl LeetCodeStats { + async fn fetch_leetcode_stats( + &self, + ctx: &Context<'_>, + member_id: i32, + username: String, + ) -> Result { + let pool = ctx.data::>()?; + let client = 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 + } + } + } + } + "#; + + let response = client + .post(url) + .header("Content-Type", "application/json") + .json(&serde_json::json!({ + "query": query, + "variables": { "username": username } + })) + .send() + .await + .map_err(|e| async_graphql::Error::new(format!("Request error: {:?}", e)))?; + + let data: Value = response + .json() + .await + .map_err(|e| async_graphql::Error::new(format!("JSON parsing error: {:?}", e)))?; + + let empty_vec = vec![]; + let submissions = data["data"]["matchedUser"]["submitStats"]["acSubmissionNum"] + .as_array() + .unwrap_or(&empty_vec); + + + let mut problems_solved = 0; + let mut easy_solved = 0; + let mut medium_solved = 0; + let mut hard_solved = 0; + + for stat in submissions { + 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 contests_participated = data["data"]["userContestRanking"]["attendedContestsCount"] + .as_i64() + .unwrap_or(0) as i32; + let rank = data["data"]["matchedUser"]["profile"]["ranking"] + .as_i64() + .unwrap_or(0) as i32; + + sqlx::query!( + r#" + 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 + "#, + member_id, + username, + problems_solved, + easy_solved, + medium_solved, + hard_solved, + contests_participated, + rank, + contests_participated + ) + .execute(pool.as_ref()) + .await + .map_err(|e| async_graphql::Error::new(format!("Database update error: {:?}", e)))?; + + Ok(true) + } +} diff --git a/src/graphql/mutations/mod.rs b/src/graphql/mutations/mod.rs index 012ed2a..822db2d 100644 --- a/src/graphql/mutations/mod.rs +++ b/src/graphql/mutations/mod.rs @@ -2,8 +2,19 @@ pub mod attendance_mutations; pub mod member_mutations; pub mod project_mutations; pub mod streak_mutations; +pub mod update_leaderboard; //leaderboard +pub mod leetcode_status; +pub mod codeforces_status; + + pub use attendance_mutations::AttendanceMutations; pub use member_mutations::MemberMutations; pub use project_mutations::ProjectMutations; pub use streak_mutations::StreakMutations; +pub use leetcode_status::LeetCodeStats; +pub use codeforces_status::CodeforcesStats; +pub use update_leaderboard::LeaderboardMutation; + + +//use any mutations for leaderboard if needed \ No newline at end of file diff --git a/src/graphql/mutations/update_leaderboard.rs b/src/graphql/mutations/update_leaderboard.rs new file mode 100644 index 0000000..0a62004 --- /dev/null +++ b/src/graphql/mutations/update_leaderboard.rs @@ -0,0 +1,113 @@ +use async_graphql::{Context, Object, Result as GqlResult}; +use sqlx::{PgPool}; +use std::collections::HashMap; +use std::sync::Arc; + +#[derive(Default)] +pub struct LeaderboardMutation; + +#[Object] +impl LeaderboardMutation { + async fn update_leaderboard(&self, ctx: &Context<'_>) -> GqlResult { + let pool = ctx.data::>() + .map_err(|_| async_graphql::Error::new("Failed to access the database pool"))?; + + + let leetcode_stats = sqlx::query!( + "SELECT member_id, problems_solved, easy_solved, medium_solved, hard_solved, + contests_participated, best_rank + FROM leetcode_stats" + ) + .fetch_all(pool.as_ref()) + .await + .map_err(|e| async_graphql::Error::new(format!("Failed to fetch LeetCode stats: {:?}", e)))?; + + + let codeforces_stats = sqlx::query!( + "SELECT member_id, codeforces_rating, max_rating, contests_participated + FROM codeforces_stats" + ) + .fetch_all(pool.as_ref()) + .await + .map_err(|e| async_graphql::Error::new(format!("Failed to fetch Codeforces stats: {:?}", e)))?; + + let cf_lookup: HashMap = codeforces_stats + .iter() + .map(|row| { + ( + row.member_id, + (row.codeforces_rating, row.max_rating, row.contests_participated), + ) + }) + .collect(); + + for row in &leetcode_stats { + let leetcode_score = (5 * row.easy_solved) + + (10 * row.medium_solved) + + (20 * row.hard_solved) + + (2 * row.contests_participated) + + (100 - row.best_rank / 10).max(0); + + let (codeforces_score, unified_score) = cf_lookup.get(&row.member_id) + .map(|(rating, max_rating, contests)| { + let cf_score = (rating / 10) + (max_rating / 20) + (5 * contests); + (cf_score, leetcode_score + cf_score) + }) + .unwrap_or((0, leetcode_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()", + row.member_id, + leetcode_score, + codeforces_score, + unified_score + ) + .execute(pool.as_ref()) + .await; + + if let Err(e) = result { + eprintln!("Failed to update leaderboard for member ID {}: {:?}", row.member_id, e); + } + } + + for row in &codeforces_stats { + if leetcode_stats.iter().any(|lc| lc.member_id == row.member_id) { + continue; + } + + let codeforces_score = (row.codeforces_rating / 10) + + (row.max_rating / 20) + + (5 * row.contests_participated); + + 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()", + row.member_id, + 0, + codeforces_score, + unified_score + ) + .execute(pool.as_ref()) + .await; + + if let Err(e) = result { + eprintln!("Failed to update leaderboard for Codeforces-only member ID {}: {:?}", row.member_id, e); + } + } + + Ok(true) + } +} From 097ee46cdd24c362ca780560115d41483a6ed52c Mon Sep 17 00:00:00 2001 From: Aadarsh Mahesh K Date: Wed, 19 Mar 2025 02:24:37 +0530 Subject: [PATCH 05/10] feat: updated queries for the leaderboard and rust structs --- src/graphql/queries/leaderboard_queries.rs | 13 +++++++------ src/graphql/queries/mod.rs | 3 +++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/graphql/queries/leaderboard_queries.rs b/src/graphql/queries/leaderboard_queries.rs index 32c94a1..ff25ff8 100644 --- a/src/graphql/queries/leaderboard_queries.rs +++ b/src/graphql/queries/leaderboard_queries.rs @@ -2,12 +2,13 @@ use async_graphql::{Context, Object}; use sqlx::PgPool; use std::sync::Arc; -use crate::db::leaderboard::{CodeforcesStatsWithName, LeaderboardWithMember, LeetCodeStatsWithName}; +use crate::models::leaderboard::{CodeforcesStatsWithName, LeaderboardWithMember, LeetCodeStatsWithName}; -pub struct QueryRoot; +#[derive(Default)] +pub struct LeaderboardQueries; #[Object] -impl QueryRoot { +impl LeaderboardQueries { async fn get_unified_leaderboard( &self, ctx: &Context<'_>, @@ -18,7 +19,7 @@ impl QueryRoot { 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 + JOIN member m ON l.member_id = m.member_id ORDER BY unified_score DESC", ) .fetch_all(pool.as_ref()) @@ -36,7 +37,7 @@ impl QueryRoot { 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 + JOIN member m ON l.member_id = m.member_id ORDER BY best_rank", ) .fetch_all(pool.as_ref()) @@ -54,7 +55,7 @@ impl QueryRoot { 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 + JOIN member m ON c.member_id = m.member_id ORDER BY max_rating DESC", ) .fetch_all(pool.as_ref()) diff --git a/src/graphql/queries/mod.rs b/src/graphql/queries/mod.rs index 49a8263..e533c1c 100644 --- a/src/graphql/queries/mod.rs +++ b/src/graphql/queries/mod.rs @@ -2,8 +2,11 @@ pub mod attendance_queries; pub mod member_queries; pub mod project_queries; pub mod streak_queries; +pub mod leaderboard_queries; + pub use attendance_queries::AttendanceQueries; pub use member_queries::MemberQueries; pub use project_queries::ProjectQueries; pub use streak_queries::StreakQueries; +pub use leaderboard_queries::LeaderboardQueries; From 30a83f82a0be9dd91e3b22723939cbe6bac77481 Mon Sep 17 00:00:00 2001 From: Aadarsh Mahesh K Date: Thu, 3 Apr 2025 22:45:15 +0530 Subject: [PATCH 06/10] feat: updated leaderboard mutations --- src/graphql/mutations/codeforces_status.rs | 6 +++--- src/graphql/mutations/leaderboard_mutation.rs | 2 +- src/graphql/mutations/leetcode_status.rs | 6 +++--- src/models/mod.rs | 1 + 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/graphql/mutations/codeforces_status.rs b/src/graphql/mutations/codeforces_status.rs index 8167966..de51dbb 100644 --- a/src/graphql/mutations/codeforces_status.rs +++ b/src/graphql/mutations/codeforces_status.rs @@ -5,11 +5,11 @@ use sqlx::PgPool; use std::sync::Arc; #[derive(Default)] -pub struct CodeforcesStats; +pub struct FetchCodeForces; #[Object] -impl CodeforcesStats { - async fn fetch_codeforces_stats( +impl FetchCodeForces { + pub async fn fetch_codeforces_stats( &self, ctx: &Context<'_>, // Retrieve the database pool from context member_id: i32, diff --git a/src/graphql/mutations/leaderboard_mutation.rs b/src/graphql/mutations/leaderboard_mutation.rs index 27c5590..52fa7d9 100644 --- a/src/graphql/mutations/leaderboard_mutation.rs +++ b/src/graphql/mutations/leaderboard_mutation.rs @@ -8,7 +8,7 @@ pub struct LeadMutation; #[Object] impl LeadMutation { - async fn add_or_update_leetcode_username( + pub async fn add_or_update_leetcode_username( &self, ctx: &Context<'_>, member_id: i32, diff --git a/src/graphql/mutations/leetcode_status.rs b/src/graphql/mutations/leetcode_status.rs index ec983b2..1eaadd4 100644 --- a/src/graphql/mutations/leetcode_status.rs +++ b/src/graphql/mutations/leetcode_status.rs @@ -5,11 +5,11 @@ use sqlx::PgPool; use std::sync::Arc; #[derive(Default)] -pub struct LeetCodeStats; +pub struct FetchLeetCode; #[Object] -impl LeetCodeStats { - async fn fetch_leetcode_stats( +impl FetchLeetCode { + pub async fn fetch_leetcode_stats( &self, ctx: &Context<'_>, member_id: i32, diff --git a/src/models/mod.rs b/src/models/mod.rs index b728230..5e8f0d6 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,4 +1,5 @@ pub mod attendance; +pub mod leaderboard; pub mod member; pub mod project; pub mod status_update; From 39fa451f83f788878fae6fbd7dc656468234e093 Mon Sep 17 00:00:00 2001 From: Aadarsh Mahesh K Date: Thu, 3 Apr 2025 22:47:45 +0530 Subject: [PATCH 07/10] feat: update leaderboard along with attendence --- src/daily_task/mod.rs | 87 +++++++++++++++++++++++++++++++++++---- src/models/leaderboard.rs | 2 - 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/src/daily_task/mod.rs b/src/daily_task/mod.rs index be21e1d..8507f49 100644 --- a/src/daily_task/mod.rs +++ b/src/daily_task/mod.rs @@ -4,10 +4,18 @@ use sqlx::PgPool; use std::sync::Arc; use tokio::time::sleep_until; use tracing::{debug, error, info}; +use async_graphql::Context; +use crate::{ + models::{ + leaderboard::{CodeforcesStats, LeetCodeStats}, + member::Member, + }, + graphql::mutations::{ + LeaderboardMutation,FetchCodeForces,FetchLeetCode, + }, +}; -use crate::models::member::Member; - -pub async fn run_daily_task_at_midnight(pool: Arc) { +pub async fn run_daily_task_at_midnight(pool: Arc,ctx: &Context<'_>) { loop { let now = chrono::Utc::now().with_timezone(&Kolkata); let naive_midnight = @@ -30,14 +38,14 @@ pub 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; - execute_daily_task(pool.clone()).await; + execute_daily_task(pool.clone(),ctx).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 -async fn execute_daily_task(pool: Arc) { +async fn execute_daily_task(pool: Arc,ctx: &Context<'_>) { // Members is queried outside of each function to avoid repetition let members = sqlx::query_as::<_, Member>("SELECT * FROM Member") .fetch_all(&*pool) @@ -47,13 +55,78 @@ async fn execute_daily_task(pool: Arc) { Ok(members) => { update_attendance(&members, &pool).await; update_status_history(&members, &pool).await; + update_leaderboard_task(pool.clone(), ctx).await; } // TODO: Handle this Err(e) => error!("Failed to fetch members: {:?}", e), }; } -async fn update_attendance(members: &Vec, pool: &PgPool) { +pub async fn update_leaderboard_task(pool: Arc,ctx: &Context<'_>) { + let members: Result, sqlx::Error> = + sqlx::query_as::<_, Member>("SELECT id FROM Member") + .fetch_all(pool.as_ref()) + .await; + + match members { + Ok(members) => { + for member in members { + // Fetch LeetCode username + let leetcode_username = sqlx::query_as::<_, LeetCodeStats>( + "SELECT leetcode_username FROM leetcode_stats WHERE member_id = $1", + ) + .bind(member.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 FetchLeetCode::fetch_leetcode_stats(&FetchLeetCode,ctx, member.member_id, username).await { + Ok(_) => println!("LeetCode stats updated for member ID: {}", member.member_id), + Err(e) => eprintln!( + "Failed to update LeetCode stats for member ID {}: {:?}", + member.member_id, e + ), + } + } + + // Fetch Codeforces username + let codeforces_username = sqlx::query_as::<_, CodeforcesStats>( + "SELECT codeforces_handle FROM codeforces_stats WHERE member_id = $1", + ) + .bind(member.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 FetchCodeForces::fetch_codeforces_stats(&FetchCodeForces, ctx,member.member_id, username).await { + Ok(_) => println!("Codeforces stats updated for member ID: {}", member.member_id), + Err(e) => eprintln!( + "Failed to update Codeforces stats for member ID {}: {:?}", + member.member_id, e + ), + } + } + + // Update leaderboard + match LeaderboardMutation.update_leaderboard(ctx).await { + Ok(_) => println!("Leaderboard updated."), + Err(e) => eprintln!("Failed to update leaderboard: {:?}", e), + } + } + } + Err(e) => eprintln!("Failed to fetch members: {:?}", e), + } +} + + + +async fn update_attendance(members: Vec, pool: &PgPool) { #[allow(deprecated)] let today = chrono::Utc::now() .with_timezone(&Kolkata) @@ -129,4 +202,4 @@ async fn update_status_history(members: &Vec, pool: &PgPool) { } } } -} +} \ No newline at end of file diff --git a/src/models/leaderboard.rs b/src/models/leaderboard.rs index 67afd6c..7f9d915 100644 --- a/src/models/leaderboard.rs +++ b/src/models/leaderboard.rs @@ -53,7 +53,6 @@ pub struct LeetCodeStatsWithName { #[derive(FromRow, SimpleObject)] pub struct CodeforcesStats { - pub id: i32, pub member_id: i32, pub codeforces_handle: String, pub codeforces_rating: i32, @@ -63,7 +62,6 @@ pub struct CodeforcesStats { #[derive(FromRow, SimpleObject)] pub struct CodeforcesStatsWithName { - pub id: i32, pub member_id: i32, pub member_name: String, pub codeforces_handle: String, From d47f864975e7a5440676e9c8f2ac4c96447f5626 Mon Sep 17 00:00:00 2001 From: Aadarsh Mahesh K Date: Thu, 3 Apr 2025 22:48:45 +0530 Subject: [PATCH 08/10] refactor: leaderboard-related mutations --- .gitignore | 1 + Cargo.lock | 38 +- .../20250312124630_add_leaderboard_tables.sql | 6 +- package-lock.json | 1572 +++++++++++++++++ package.json | 7 + src/daily_task/mod.rs | 32 +- src/graphql/integration_test.rs | 212 +++ src/graphql/mod.rs | 9 +- src/graphql/mutations/codeforces_status.rs | 68 +- src/graphql/mutations/leaderboard_api.rs | 264 +++ src/graphql/mutations/leetcode_status.rs | 101 +- src/graphql/mutations/mod.rs | 9 +- src/graphql/mutations/update_leaderboard.rs | 2 +- src/graphql/queries/codeforces_status.rs | 53 - src/graphql/queries/leetcode_status.rs | 112 -- 15 files changed, 2128 insertions(+), 358 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/graphql/integration_test.rs create mode 100644 src/graphql/mutations/leaderboard_api.rs delete mode 100644 src/graphql/queries/codeforces_status.rs delete mode 100644 src/graphql/queries/leetcode_status.rs diff --git a/.gitignore b/.gitignore index c12db7a..05a99e6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ Secrets*.toml backups/ .env *.log + diff --git a/Cargo.lock b/Cargo.lock index 0c7d73c..31ed16e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,8 +76,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "036618f842229ba0b89652ffe425f96c7c16a49f7e3cb23b56fca7f61fd74980" dependencies = [ "async-graphql-derive", - "async-graphql-parser", - "async-graphql-value", + "async-graphql-parser 7.0.15", + "async-graphql-value 7.0.15", "async-stream", "async-trait", "base64 0.22.1", @@ -89,7 +89,7 @@ dependencies = [ "futures-util", "handlebars", "http", - "indexmap", + "indexmap 2.7.1", "mime", "multer", "num-traits", @@ -127,7 +127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd45deb3dbe5da5cdb8d6a670a7736d735ba65b455328440f236dfb113727a3d" dependencies = [ "Inflector", - "async-graphql-parser", + "async-graphql-parser 7.0.15", "darling", "proc-macro-crate", "proc-macro2", @@ -143,7 +143,7 @@ version = "7.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b7607e59424a35dadbc085b0d513aa54ec28160ee640cf79ec3b634eba66d3" dependencies = [ - "async-graphql-value", + "async-graphql-value 7.0.15", "pest", "serde", "serde_json", @@ -156,7 +156,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ecdaff7c9cffa3614a9f9999bf9ee4c3078fe3ce4d6a6e161736b56febf2de" dependencies = [ "bytes", - "indexmap", + "indexmap 2.7.1", "serde", "serde_json", ] @@ -892,7 +892,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.7.1", "slab", "tokio", "tokio-util", @@ -913,6 +913,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -1265,6 +1271,17 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.7.1" @@ -2010,6 +2027,7 @@ version = "0.1.0" dependencies = [ "async-graphql", "async-graphql-axum", + "async-graphql-parser 2.11.3", "axum", "chrono", "chrono-tz", @@ -2363,8 +2381,8 @@ dependencies = [ "futures-io", "futures-util", "hashbrown 0.15.2", - "hashlink", - "indexmap", + "hashlink 0.10.0", + "indexmap 2.7.1", "log", "memchr", "once_cell", @@ -2873,7 +2891,7 @@ version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ - "indexmap", + "indexmap 2.7.1", "serde", "serde_spanned", "toml_datetime", diff --git a/migrations/20250312124630_add_leaderboard_tables.sql b/migrations/20250312124630_add_leaderboard_tables.sql index 544e97f..89ff540 100644 --- a/migrations/20250312124630_add_leaderboard_tables.sql +++ b/migrations/20250312124630_add_leaderboard_tables.sql @@ -1,6 +1,6 @@ -- Add migration script here -CREATE TABLE leaderboard ( +CREATE TABLE IF NOT EXISTS leaderboard ( id SERIAL PRIMARY KEY, member_id INT UNIQUE NOT NULL, leetcode_score INT, @@ -10,7 +10,7 @@ CREATE TABLE leaderboard ( FOREIGN KEY (member_id) REFERENCES member(member_id) ); -CREATE TABLE leetcode_stats ( +CREATE TABLE IF NOT EXISTS leetcode_stats ( id SERIAL PRIMARY KEY, member_id INT NOT NULL, leetcode_username VARCHAR(255) NOT NULL, @@ -24,7 +24,7 @@ CREATE TABLE leetcode_stats ( FOREIGN KEY (member_id) REFERENCES member(member_id) ); -CREATE TABLE codeforces_stats ( +CREATE TABLE IF NOT EXISTS codeforces_stats ( id SERIAL PRIMARY KEY, member_id INT NOT NULL, codeforces_handle VARCHAR(255) NOT NULL, diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e0641ff --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1572 @@ +{ + "name": "root", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@apollo/client": "^3.13.4", + "graphiql": "^3.8.3", + "graphql": "^16.10.0" + } + }, + "node_modules/@apollo/client": { + "version": "3.13.4", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.13.4.tgz", + "integrity": "sha512-Ot3RaN2M/rhIKDqXBdOVlN0dQbHydUrYJ9lTxkvd6x7W1pAjwduUccfoz2gsO4U9by7oWtRj/ySF0MFNUp+9Aw==", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@wry/caches": "^1.0.0", + "@wry/equality": "^0.5.6", + "@wry/trie": "^0.5.0", + "graphql-tag": "^2.12.6", + "hoist-non-react-statics": "^3.3.2", + "optimism": "^0.18.0", + "prop-types": "^15.7.2", + "rehackt": "^0.1.0", + "symbol-observable": "^4.0.0", + "ts-invariant": "^0.10.3", + "tslib": "^2.3.0", + "zen-observable-ts": "^1.2.5" + }, + "peerDependencies": { + "graphql": "^15.0.0 || ^16.0.0", + "graphql-ws": "^5.5.5 || ^6.0.3", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", + "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" + }, + "peerDependenciesMeta": { + "graphql-ws": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "subscriptions-transport-ws": { + "optional": true + } + } + }, + "node_modules/@codemirror/language": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.0.0.tgz", + "integrity": "sha512-rtjk5ifyMzOna1c7PBu7J1VCt0PvA5wy3o8eMVnxMKb7z8KA7JFecvD04dSn14vj/bBaAbqRsGed5OjtofEnLA==", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "peer": true, + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.36.4", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.4.tgz", + "integrity": "sha512-ZQ0V5ovw/miKEXTvjgzRyjnrk9TwriUB1k4R5p7uNnHR9Hus+D1SXHGdJshijEzPFjU25xea/7nhIeSqYFKdbA==", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.5.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "optional": true, + "dependencies": { + "@emotion/memoize": "0.7.4" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "optional": true + }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==" + }, + "node_modules/@graphiql/react": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@graphiql/react/-/react-0.28.2.tgz", + "integrity": "sha512-6PE2Ff9dXpyQMHy3oKzCjT738kY2+wdQ4Xce8+1K+G2yMGZ716Fo0i4vW3S6ErHVI4S/C76gFTQlv/pzxU5ylg==", + "dependencies": { + "@graphiql/toolkit": "^0.11.0", + "@headlessui/react": "^1.7.15", + "@radix-ui/react-dialog": "^1.0.4", + "@radix-ui/react-dropdown-menu": "^2.0.5", + "@radix-ui/react-tooltip": "^1.0.6", + "@radix-ui/react-visually-hidden": "^1.0.3", + "@types/codemirror": "^5.60.8", + "clsx": "^1.2.1", + "codemirror": "^5.65.3", + "codemirror-graphql": "^2.2.0", + "copy-to-clipboard": "^3.2.0", + "framer-motion": "^6.5.1", + "get-value": "^3.0.1", + "graphql-language-service": "^5.3.0", + "markdown-it": "^14.1.0", + "react-compiler-runtime": "19.0.0-beta-37ed2a7-20241206", + "set-value": "^4.1.0" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/@graphiql/toolkit": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.11.1.tgz", + "integrity": "sha512-G02te70/oYYna5UhbH6TXwNxeQyWa+ChlPonUrKwC5Ot9ItraGJ9yUU4sS+YRaA+EvkzNoHG79XcW2k1QaAMiw==", + "dependencies": { + "@n1ru4l/push-pull-async-iterable-iterator": "^3.1.0", + "meros": "^1.1.4" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "graphql-ws": ">= 4.5.0" + }, + "peerDependenciesMeta": { + "graphql-ws": { + "optional": true + } + } + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@headlessui/react": { + "version": "1.7.19", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz", + "integrity": "sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==", + "dependencies": { + "@tanstack/react-virtual": "^3.0.0-beta.60", + "client-only": "^0.0.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "peer": true + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "peer": true, + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "peer": true, + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "peer": true + }, + "node_modules/@motionone/animation": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.18.0.tgz", + "integrity": "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==", + "dependencies": { + "@motionone/easing": "^10.18.0", + "@motionone/types": "^10.17.1", + "@motionone/utils": "^10.18.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/dom": { + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.12.0.tgz", + "integrity": "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==", + "dependencies": { + "@motionone/animation": "^10.12.0", + "@motionone/generators": "^10.12.0", + "@motionone/types": "^10.12.0", + "@motionone/utils": "^10.12.0", + "hey-listen": "^1.0.8", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/easing": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.18.0.tgz", + "integrity": "sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg==", + "dependencies": { + "@motionone/utils": "^10.18.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/generators": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.18.0.tgz", + "integrity": "sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg==", + "dependencies": { + "@motionone/types": "^10.17.1", + "@motionone/utils": "^10.18.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/types": { + "version": "10.17.1", + "resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.17.1.tgz", + "integrity": "sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A==" + }, + "node_modules/@motionone/utils": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.18.0.tgz", + "integrity": "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==", + "dependencies": { + "@motionone/types": "^10.17.1", + "hey-listen": "^1.0.8", + "tslib": "^2.3.1" + } + }, + "node_modules/@n1ru4l/push-pull-async-iterable-iterator": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-3.2.0.tgz", + "integrity": "sha512-3fkKj25kEjsfObL6IlKPAlHYPq/oYwUkkQ03zsTTiDjD7vg/RxjdiLeCydqtxHZP0JgsXL3D/X5oAkMGzuUp/Q==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz", + "integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz", + "integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz", + "integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", + "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.6.tgz", + "integrity": "sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", + "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.6.tgz", + "integrity": "sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz", + "integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", + "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", + "dependencies": { + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", + "integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz", + "integrity": "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "dependencies": { + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz", + "integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.4.tgz", + "integrity": "sha512-jPWC3BXvVLHsMX67NEHpJaZ+/FySoNxFfBEiF4GBc1+/nVwdRm+UcSCYnKP3pXQr0eEsDpXi/PQZhNfJNopH0g==", + "dependencies": { + "@tanstack/virtual-core": "3.13.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.4", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.4.tgz", + "integrity": "sha512-fNGO9fjjSLns87tlcto106enQQLycCKR4DPNpgq3djP5IdcPFdPAmaKjsgzIeRhH7hWrELgW12hYnRthS5kLUw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@types/codemirror": { + "version": "5.60.15", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.15.tgz", + "integrity": "sha512-dTOvwEQ+ouKJ/rE9LT1Ue2hmP6H1mZv5+CCnNWu2qtiOe2LQa9lCprEY20HxiDmV/Bxh+dXjywmy5aKvoGjULA==", + "dependencies": { + "@types/tern": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + }, + "node_modules/@types/tern": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", + "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@wry/caches": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz", + "integrity": "sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/context": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.4.tgz", + "integrity": "sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/equality": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.7.tgz", + "integrity": "sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/trie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.5.0.tgz", + "integrity": "sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/codemirror": { + "version": "5.65.18", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.18.tgz", + "integrity": "sha512-Gaz4gHnkbHMGgahNt3CA5HBk5lLQBqmD/pBgeB4kQU6OedZmqMBjlRF0LSrp2tJ4wlLNPm2FfaUd1pDy0mdlpA==" + }, + "node_modules/codemirror-graphql": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-2.2.0.tgz", + "integrity": "sha512-egIiewf5zEH5LLSkJpJDpYxO1OkNruD0gTWiBrS1JmXk7yjt5WPw7jSmDRkWJx8JheHONltaJPNPWdTUT5LRIQ==", + "dependencies": { + "@types/codemirror": "^0.0.90", + "graphql-language-service": "5.3.0" + }, + "peerDependencies": { + "@codemirror/language": "6.0.0", + "codemirror": "^5.65.3", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/codemirror-graphql/node_modules/@types/codemirror": { + "version": "0.0.90", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.90.tgz", + "integrity": "sha512-8Z9+tSg27NPRGubbUPUCrt5DDG/OWzLph5BvcDykwR5D7RyZh5mhHG0uS1ePKV1YFCA+/cwc4Ey2AJAEFfV3IA==", + "dependencies": { + "@types/tern": "*" + } + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/debounce-promise": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/debounce-promise/-/debounce-promise-3.1.2.tgz", + "integrity": "sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg==" + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/framer-motion": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-6.5.1.tgz", + "integrity": "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==", + "dependencies": { + "@motionone/dom": "10.12.0", + "framesync": "6.0.1", + "hey-listen": "^1.0.8", + "popmotion": "11.0.3", + "style-value-types": "5.0.0", + "tslib": "^2.1.0" + }, + "optionalDependencies": { + "@emotion/is-prop-valid": "^0.8.2" + }, + "peerDependencies": { + "react": ">=16.8 || ^17.0.0 || ^18.0.0", + "react-dom": ">=16.8 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/framesync": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.0.1.tgz", + "integrity": "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-value": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-3.0.1.tgz", + "integrity": "sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/graphiql": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/graphiql/-/graphiql-3.8.3.tgz", + "integrity": "sha512-cuPDYtXVKV86Pu5PHBX642Odi/uVEE2y1Jxq5rGO/Qy1G2lRp7ZZ7a/T30RzxhuLSWo9zUbzq0P3U49//H0Ugw==", + "dependencies": { + "@graphiql/react": "^0.28.2" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/graphql": { + "version": "16.10.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.10.0.tgz", + "integrity": "sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/graphql-language-service": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-5.3.0.tgz", + "integrity": "sha512-gCQIIy7lM9CB1KPLEb+DNZLczA9zuTLEOJE2hEQZTFYInogdmMDRa6RAkvM4LL0LcgcS+3uPs6KtHlcjCqRbUg==", + "dependencies": { + "debounce-promise": "^3.1.2", + "nullthrows": "^1.0.0", + "vscode-languageserver-types": "^3.17.1" + }, + "bin": { + "graphql": "dist/temp-bin.js" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2" + } + }, + "node_modules/graphql-tag": { + "version": "2.12.6", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", + "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/hey-listen": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", + "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==" + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-primitive": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz", + "integrity": "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" + }, + "node_modules/meros": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/meros/-/meros-1.3.0.tgz", + "integrity": "sha512-2BNGOimxEz5hmjUG2FwoxCt5HN7BXdaWyFqEwxPTrJzVdABtrL4TiHTcsWSFAxPQ/tOnEaQEJh3qWq71QRMY+w==", + "engines": { + "node": ">=13" + }, + "peerDependencies": { + "@types/node": ">=13" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optimism": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.18.1.tgz", + "integrity": "sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ==", + "dependencies": { + "@wry/caches": "^1.0.0", + "@wry/context": "^0.7.0", + "@wry/trie": "^0.5.0", + "tslib": "^2.3.0" + } + }, + "node_modules/popmotion": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.3.tgz", + "integrity": "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==", + "dependencies": { + "framesync": "6.0.1", + "hey-listen": "^1.0.8", + "style-value-types": "5.0.0", + "tslib": "^2.1.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-compiler-runtime": { + "version": "19.0.0-beta-37ed2a7-20241206", + "resolved": "https://registry.npmjs.org/react-compiler-runtime/-/react-compiler-runtime-19.0.0-beta-37ed2a7-20241206.tgz", + "integrity": "sha512-9e6rCpVylr9EnVocgYAjft7+2v01BDpajeHKRoO+oc9pKcAMTpstHtHvE/TSVbyf4FvzCGjfKcfHM9XGTXI6Tw==", + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-remove-scroll": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/rehackt": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/rehackt/-/rehackt-0.1.0.tgz", + "integrity": "sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw==", + "peerDependencies": { + "@types/react": "*", + "react": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/set-value": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-4.1.0.tgz", + "integrity": "sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==", + "funding": [ + "https://github.com/sponsors/jonschlinkert", + "https://paypal.me/jonathanschlinkert", + "https://jonschlinkert.dev/sponsor" + ], + "dependencies": { + "is-plain-object": "^2.0.4", + "is-primitive": "^3.0.1" + }, + "engines": { + "node": ">=11.0" + } + }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "peer": true + }, + "node_modules/style-value-types": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.0.0.tgz", + "integrity": "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==", + "dependencies": { + "hey-listen": "^1.0.8", + "tslib": "^2.1.0" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, + "node_modules/ts-invariant": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", + "integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "peer": true + }, + "node_modules/zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" + }, + "node_modules/zen-observable-ts": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz", + "integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==", + "dependencies": { + "zen-observable": "0.8.15" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9858870 --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "@apollo/client": "^3.13.4", + "graphiql": "^3.8.3", + "graphql": "^16.10.0" + } +} diff --git a/src/daily_task/mod.rs b/src/daily_task/mod.rs index 8507f49..f461db8 100644 --- a/src/daily_task/mod.rs +++ b/src/daily_task/mod.rs @@ -4,18 +4,16 @@ use sqlx::PgPool; use std::sync::Arc; use tokio::time::sleep_until; use tracing::{debug, error, info}; -use async_graphql::Context; +use crate::graphql::mutations::{fetch_and_update_codeforces_stats,fetch_and_update_leetcode,update_leaderboard_scores}; + use crate::{ models::{ leaderboard::{CodeforcesStats, LeetCodeStats}, member::Member, }, - graphql::mutations::{ - LeaderboardMutation,FetchCodeForces,FetchLeetCode, - }, }; -pub async fn run_daily_task_at_midnight(pool: Arc,ctx: &Context<'_>) { +pub async fn run_daily_task_at_midnight(pool: Arc) { loop { let now = chrono::Utc::now().with_timezone(&Kolkata); let naive_midnight = @@ -38,14 +36,14 @@ pub async fn run_daily_task_at_midnight(pool: Arc,ctx: &Context<'_>) { 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(),ctx).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 -async fn execute_daily_task(pool: Arc,ctx: &Context<'_>) { +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) @@ -55,16 +53,23 @@ async fn execute_daily_task(pool: Arc,ctx: &Context<'_>) { Ok(members) => { update_attendance(&members, &pool).await; update_status_history(&members, &pool).await; - update_leaderboard_task(pool.clone(), ctx).await; + update_leaderboard_task(pool.clone()).await; } // TODO: Handle this Err(e) => error!("Failed to fetch members: {:?}", e), }; } -pub async fn update_leaderboard_task(pool: Arc,ctx: &Context<'_>) { +pub async fn update_leaderboard_task(pool: Arc) { + #[allow(deprecated)] + let today = chrono::Utc::now() + .with_timezone(&Kolkata) + .date() + .naive_local(); + debug!("Updating leaderboard on {}", today); + let members: Result, sqlx::Error> = - sqlx::query_as::<_, Member>("SELECT id FROM Member") + sqlx::query_as::<_, Member>("SELECT * FROM Member") .fetch_all(pool.as_ref()) .await; @@ -83,7 +88,7 @@ pub async fn update_leaderboard_task(pool: Arc,ctx: &Context<'_>) { let username = leetcode_stats.leetcode_username.clone(); // Fetch and update LeetCode stats - match FetchLeetCode::fetch_leetcode_stats(&FetchLeetCode,ctx, member.member_id, username).await { + match fetch_and_update_leetcode(pool.clone(), member.member_id, &username).await { Ok(_) => println!("LeetCode stats updated for member ID: {}", member.member_id), Err(e) => eprintln!( "Failed to update LeetCode stats for member ID {}: {:?}", @@ -104,7 +109,8 @@ pub async fn update_leaderboard_task(pool: Arc,ctx: &Context<'_>) { let username = codeforces_stats.codeforces_handle.clone(); // Fetch and update Codeforces stats - match FetchCodeForces::fetch_codeforces_stats(&FetchCodeForces, ctx,member.member_id, username).await { + match fetch_and_update_codeforces_stats(pool.clone(), member.member_id, &username).await + { Ok(_) => println!("Codeforces stats updated for member ID: {}", member.member_id), Err(e) => eprintln!( "Failed to update Codeforces stats for member ID {}: {:?}", @@ -114,7 +120,7 @@ pub async fn update_leaderboard_task(pool: Arc,ctx: &Context<'_>) { } // Update leaderboard - match LeaderboardMutation.update_leaderboard(ctx).await { + match update_leaderboard_scores(pool.clone()).await { Ok(_) => println!("Leaderboard updated."), Err(e) => eprintln!("Failed to update leaderboard: {:?}", e), } diff --git a/src/graphql/integration_test.rs b/src/graphql/integration_test.rs new file mode 100644 index 0000000..89c46d2 --- /dev/null +++ b/src/graphql/integration_test.rs @@ -0,0 +1,212 @@ +use config::{Config, File, FileFormat}; +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::sync::Arc; + +pub fn get_database_url() -> String { + // Create a configuration instance to read Secrets.toml + let settings = Config::builder() + .add_source(File::new("Secrets", FileFormat::Toml)) + .build() + .expect("Failed to load Secrets.toml"); + + // Retrieve the `DATABASE_URL` from the file + settings + .get_string("DATABASE_URL") + .expect("Missing 'DATABASE_URL' in Secrets.toml") +} + +// Helper function to create a test database connection +async fn setup_test_db() -> PgPool { + let database_url = get_database_url(); + PgPoolOptions::new() + .max_connections(5) + .connect(&database_url) + .await + .expect("Failed to create test database pool") +} + +// Helper function to clean up test data +async fn cleanup_test_data(pool: &PgPool) { + // sqlx::query("DELETE FROM leaderboard") + // .execute(pool) + // .await + // .unwrap(); + // sqlx::query("DELETE FROM leetcode_stats") + // .execute(pool) + // .await + // .unwrap(); + // sqlx::query("DELETE FROM codeforces_stats") + // .execute(pool) + // .await + // .unwrap(); + // sqlx::query("DELETE FROM Member") + // .execute(pool) + // .await + // .unwrap(); +} + +//test +#[tokio::test] +async fn test_insert_members_and_update_stats() { + let pool = setup_test_db().await; + + cleanup_test_data(&pool).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"), + "swayam-agrahari", + "tourist", + ), + ( + "B21CS5678", + "Jane Smith", + "Hostel B", + "jane.smith@example.com", + "Female", + 2021, + "66:77:88:99:AA:BB", + Some("jane_discord"), + "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) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + 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) + .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.8) + .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.9) + .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.8)) { + 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); + // Uncomment to fail test on fetch error + // panic!("Failed to fetch Codeforces stats") + } + } + } + + // 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" + ); + } + + // Clean up + cleanup_test_data(&pool).await; +} + +// Additional helper test to verify database connections and basic operations +#[tokio::test] +async fn test_database_connection() { + let pool = setup_test_db().await; + let database_url = get_database_url(); + + // Print the URL to verify (optional, for debugging purposes) + println!("Database URL: {}", database_url); + + // Basic database connectivity test + let result = sqlx::query("SELECT 1").fetch_one(&pool).await; + + assert!(result.is_ok(), "Database connection and query should work"); +} \ No newline at end of file diff --git a/src/graphql/mod.rs b/src/graphql/mod.rs index 0d99324..d81397b 100644 --- a/src/graphql/mod.rs +++ b/src/graphql/mod.rs @@ -1,6 +1,6 @@ use async_graphql::MergedObject; -use mutations::{AttendanceMutations, MemberMutations, ProjectMutations, StreakMutations}; -use queries::{AttendanceQueries, MemberQueries, ProjectQueries, StreakQueries}; +use mutations::{AttendanceMutations, MemberMutations, ProjectMutations, StreakMutations,FetchLeetCode,FetchCodeForces,LeaderboardMutation}; +use queries::{AttendanceQueries, MemberQueries, ProjectQueries, StreakQueries, LeaderboardQueries}; pub mod mutations; pub mod queries; @@ -11,6 +11,7 @@ pub struct Query( AttendanceQueries, StreakQueries, ProjectQueries, + LeaderboardQueries, ); #[derive(MergedObject, Default)] @@ -19,4 +20,8 @@ pub struct Mutation( AttendanceMutations, StreakMutations, ProjectMutations, + FetchLeetCode, + FetchCodeForces, + LeaderboardMutation, + ); diff --git a/src/graphql/mutations/codeforces_status.rs b/src/graphql/mutations/codeforces_status.rs index de51dbb..4c45c7e 100644 --- a/src/graphql/mutations/codeforces_status.rs +++ b/src/graphql/mutations/codeforces_status.rs @@ -1,8 +1,7 @@ use async_graphql::{Context, Object, Result}; -use reqwest; -use serde_json::Value; use sqlx::PgPool; use std::sync::Arc; +use super::leaderboard_api::fetch_and_update_codeforces_stats; #[derive(Default)] pub struct FetchCodeForces; @@ -11,67 +10,12 @@ pub struct FetchCodeForces; impl FetchCodeForces { pub async fn fetch_codeforces_stats( &self, - ctx: &Context<'_>, // Retrieve the database pool from context + ctx: &Context<'_>, member_id: i32, username: String, - ) -> Result { - let pool = ctx.data::>()?; // Get the PgPool from context - - 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; - - 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 - "#, - member_id, - username, - codeforces_rating, - max_rating, - contests_participated - ) - .execute(pool.as_ref()) - .await; - - match update_result { - Ok(_) => Ok(format!( - "Codeforces stats updated successfully for member ID: {}", - member_id - )), - Err(e) => Err(format!( - "Failed to update Codeforces stats for member ID {}: {:?}", - member_id, e - ) - .into()), - } - } else { - Err("Invalid response from Codeforces API".into()) - } - } else { - Err("Codeforces API returned an error".into()) - } + ) -> Result { + let pool = ctx.data::>()?; + fetch_and_update_codeforces_stats(pool.clone(), member_id, &username).await?; + Ok(true) } } diff --git a/src/graphql/mutations/leaderboard_api.rs b/src/graphql/mutations/leaderboard_api.rs new file mode 100644 index 0000000..1286762 --- /dev/null +++ b/src/graphql/mutations/leaderboard_api.rs @@ -0,0 +1,264 @@ +use reqwest; +use serde_json::Value; +use sqlx::PgPool; +use std::sync::Arc; +use std::collections::HashMap; +use reqwest::Client; + + +pub async fn fetch_and_update_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()) +} + +pub async fn update_leaderboard_scores(pool: Arc) -> Result<(), sqlx::Error> { + let leetcode_stats = sqlx::query!( + "SELECT member_id, problems_solved, easy_solved, medium_solved, hard_solved, + contests_participated, best_rank + FROM leetcode_stats" + ) + .fetch_all(pool.as_ref()) + .await?; + + let codeforces_stats = sqlx::query!( + "SELECT member_id, codeforces_rating, max_rating, contests_participated + FROM codeforces_stats" + ) + .fetch_all(pool.as_ref()) + .await?; + + let cf_lookup: HashMap = codeforces_stats + .iter() + .map(|row| { + ( + row.member_id, + (row.codeforces_rating, row.max_rating, row.contests_participated), + ) + }) + .collect(); + + for row in &leetcode_stats { + let leetcode_score = (5 * row.easy_solved) + + (10 * row.medium_solved) + + (20 * row.hard_solved) + + (2 * row.contests_participated) + + (100 - row.best_rank / 10).max(0); + + let (codeforces_score, unified_score) = cf_lookup.get(&row.member_id) + .map(|(rating, max_rating, contests)| { + let cf_score = (rating / 10) + (max_rating / 20) + (5 * contests); + (cf_score, leetcode_score + cf_score) + }) + .unwrap_or((0, leetcode_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()", + row.member_id, + leetcode_score, + codeforces_score, + unified_score + ) + .execute(pool.as_ref()) + .await; + + if let Err(e) = result { + eprintln!("Failed to update leaderboard for member ID {}: {:?}", row.member_id, e); + } + } + + for row in &codeforces_stats { + if leetcode_stats.iter().any(|lc| lc.member_id == row.member_id) { + continue; + } + + let codeforces_score = (row.codeforces_rating / 10) + + (row.max_rating / 20) + + (5 * row.contests_participated); + + 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()", + row.member_id, + 0, + codeforces_score, + unified_score + ) + .execute(pool.as_ref()) + .await; + + if let Err(e) = result { + eprintln!("Failed to update leaderboard for Codeforces-only member ID {}: {:?}", row.member_id, e); + } + } + + Ok(()) +} + + + +pub async fn fetch_and_update_leetcode( + pool: Arc, + member_id: i32, + username: &str, +) -> Result<(), Box> { + let client = 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 + } + } + } + } + "#; + + 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 empty_vec = vec![]; + let submissions = data["data"]["matchedUser"]["submitStats"]["acSubmissionNum"] + .as_array() + .unwrap_or(&empty_vec); + + let mut problems_solved = 0; + let mut easy_solved = 0; + let mut medium_solved = 0; + let mut hard_solved = 0; + + for stat in submissions { + 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 contests_participated = data["data"]["userContestRanking"]["attendedContestsCount"] + .as_i64() + .unwrap_or(0) as i32; + let rank = data["data"]["matchedUser"]["profile"]["ranking"] + .as_i64() + .unwrap_or(0) as i32; + + sqlx::query!( + r#" + 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 + "#, + member_id, + username, + problems_solved, + easy_solved, + medium_solved, + hard_solved, + contests_participated, + rank, + contests_participated + ) + .execute(pool.as_ref()) + .await?; + + Ok(()) +} diff --git a/src/graphql/mutations/leetcode_status.rs b/src/graphql/mutations/leetcode_status.rs index 1eaadd4..6c7e69b 100644 --- a/src/graphql/mutations/leetcode_status.rs +++ b/src/graphql/mutations/leetcode_status.rs @@ -1,8 +1,7 @@ use async_graphql::{Context, Object, Result}; -use reqwest::Client; -use serde_json::Value; use sqlx::PgPool; use std::sync::Arc; +use super::leaderboard_api::fetch_and_update_leetcode; #[derive(Default)] pub struct FetchLeetCode; @@ -16,103 +15,7 @@ impl FetchLeetCode { username: String, ) -> Result { let pool = ctx.data::>()?; - let client = 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 - } - } - } - } - "#; - - let response = client - .post(url) - .header("Content-Type", "application/json") - .json(&serde_json::json!({ - "query": query, - "variables": { "username": username } - })) - .send() - .await - .map_err(|e| async_graphql::Error::new(format!("Request error: {:?}", e)))?; - - let data: Value = response - .json() - .await - .map_err(|e| async_graphql::Error::new(format!("JSON parsing error: {:?}", e)))?; - - let empty_vec = vec![]; - let submissions = data["data"]["matchedUser"]["submitStats"]["acSubmissionNum"] - .as_array() - .unwrap_or(&empty_vec); - - - let mut problems_solved = 0; - let mut easy_solved = 0; - let mut medium_solved = 0; - let mut hard_solved = 0; - - for stat in submissions { - 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 contests_participated = data["data"]["userContestRanking"]["attendedContestsCount"] - .as_i64() - .unwrap_or(0) as i32; - let rank = data["data"]["matchedUser"]["profile"]["ranking"] - .as_i64() - .unwrap_or(0) as i32; - - sqlx::query!( - r#" - 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 - "#, - member_id, - username, - problems_solved, - easy_solved, - medium_solved, - hard_solved, - contests_participated, - rank, - contests_participated - ) - .execute(pool.as_ref()) - .await - .map_err(|e| async_graphql::Error::new(format!("Database update error: {:?}", e)))?; - + fetch_and_update_leetcode(pool.clone(), member_id, &username).await?; Ok(true) } } diff --git a/src/graphql/mutations/mod.rs b/src/graphql/mutations/mod.rs index 822db2d..2bbe32a 100644 --- a/src/graphql/mutations/mod.rs +++ b/src/graphql/mutations/mod.rs @@ -5,16 +5,19 @@ pub mod streak_mutations; pub mod update_leaderboard; //leaderboard pub mod leetcode_status; pub mod codeforces_status; - +pub mod leaderboard_api; pub use attendance_mutations::AttendanceMutations; pub use member_mutations::MemberMutations; pub use project_mutations::ProjectMutations; pub use streak_mutations::StreakMutations; -pub use leetcode_status::LeetCodeStats; -pub use codeforces_status::CodeforcesStats; +pub use leetcode_status::FetchLeetCode; +pub use codeforces_status::FetchCodeForces; pub use update_leaderboard::LeaderboardMutation; +pub use leaderboard_api::fetch_and_update_codeforces_stats; +pub use leaderboard_api::fetch_and_update_leetcode; +pub use leaderboard_api::update_leaderboard_scores; //use any mutations for leaderboard if needed \ No newline at end of file diff --git a/src/graphql/mutations/update_leaderboard.rs b/src/graphql/mutations/update_leaderboard.rs index 0a62004..3e57c0b 100644 --- a/src/graphql/mutations/update_leaderboard.rs +++ b/src/graphql/mutations/update_leaderboard.rs @@ -8,7 +8,7 @@ pub struct LeaderboardMutation; #[Object] impl LeaderboardMutation { - async fn update_leaderboard(&self, ctx: &Context<'_>) -> GqlResult { + pub async fn update_leaderboard(&self, ctx: &Context<'_>) -> GqlResult { let pool = ctx.data::>() .map_err(|_| async_graphql::Error::new("Failed to access the database pool"))?; diff --git a/src/graphql/queries/codeforces_status.rs b/src/graphql/queries/codeforces_status.rs deleted file mode 100644 index 726a815..0000000 --- a/src/graphql/queries/codeforces_status.rs +++ /dev/null @@ -1,53 +0,0 @@ -use serde_json::Value; -use sqlx::PgPool; -use std::sync::Arc; - -pub async fn fetch_codeforces_stats( - pool: Arc, - member_id: i32, - username: &str, -) -> Result<(), Box> { - let url = format!("https://codeforces.com/api/user.info?handles={}", username); - let response = reqwest::get(&url).await?; - let data: Value = response.json().await?; - - if let Some(user) = data["result"].as_array().and_then(|arr| arr.first()) { - let rating = user["rating"].as_i64().unwrap_or(0) as i32; - let max_rating = user["maxRating"].as_i64().unwrap_or(0) as i32; - let rank = user["rank"].as_str().unwrap_or("Unrated").to_string(); - let max_rank = user["maxRank"].as_str().unwrap_or("Unrated").to_string(); - - let update_result = sqlx::query( - " - INSERT INTO codeforces_stats ( - member_id, codeforces_username, rating, max_rating, rank, max_rank - ) - VALUES ($1, $2, $3, $4, $5, $6) - ON CONFLICT (member_id) DO UPDATE SET - codeforces_username = EXCLUDED.codeforces_username, - rating = EXCLUDED.rating, - max_rating = EXCLUDED.max_rating, - rank = EXCLUDED.rank, - max_rank = EXCLUDED.max_rank - " - ) - .bind(member_id) - .bind(username) - .bind(rating) - .bind(max_rating) - .bind(rank) - .bind(max_rank) - .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 - ), - } - } - - Ok(()) -} diff --git a/src/graphql/queries/leetcode_status.rs b/src/graphql/queries/leetcode_status.rs deleted file mode 100644 index d33b2c4..0000000 --- a/src/graphql/queries/leetcode_status.rs +++ /dev/null @@ -1,112 +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 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(contests_participated) - .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(()) -} \ No newline at end of file From ffd36c4c8ff282b7b3137e226251fe7a2a82a445 Mon Sep 17 00:00:00 2001 From: Aadarsh Date: Tue, 15 Jul 2025 12:08:29 +0530 Subject: [PATCH 09/10] refactor:auto-formated code using rustfmt --- src/daily_task/mod.rs | 39 ++++++++++++-------- src/graphql/mod.rs | 10 ++++-- src/graphql/mutations/codeforces_status.rs | 2 +- src/graphql/mutations/leaderboard_api.rs | 38 ++++++++++++-------- src/graphql/mutations/leetcode_status.rs | 2 +- src/graphql/mutations/mod.rs | 22 ++++++------ src/graphql/mutations/update_leaderboard.rs | 40 ++++++++++++++------- src/graphql/queries/leaderboard_queries.rs | 6 ++-- src/graphql/queries/mod.rs | 5 ++- src/models/leaderboard.rs | 2 +- 10 files changed, 102 insertions(+), 64 deletions(-) diff --git a/src/daily_task/mod.rs b/src/daily_task/mod.rs index f461db8..0bc4c5a 100644 --- a/src/daily_task/mod.rs +++ b/src/daily_task/mod.rs @@ -1,16 +1,16 @@ use chrono::NaiveTime; +use crate::graphql::mutations::{ + fetch_and_update_codeforces_stats, fetch_and_update_leetcode, update_leaderboard_scores, +}; use chrono_tz::Asia::Kolkata; use sqlx::PgPool; use std::sync::Arc; use tokio::time::sleep_until; use tracing::{debug, error, info}; -use crate::graphql::mutations::{fetch_and_update_codeforces_stats,fetch_and_update_leetcode,update_leaderboard_scores}; -use crate::{ - models::{ - leaderboard::{CodeforcesStats, LeetCodeStats}, - member::Member, - }, +use crate::models::{ + leaderboard::{CodeforcesStats, LeetCodeStats}, + member::Member, }; pub async fn run_daily_task_at_midnight(pool: Arc) { @@ -88,10 +88,13 @@ pub async fn update_leaderboard_task(pool: Arc) { let username = leetcode_stats.leetcode_username.clone(); // Fetch and update LeetCode stats - match fetch_and_update_leetcode(pool.clone(), member.member_id, &username).await { - Ok(_) => println!("LeetCode stats updated for member ID: {}", member.member_id), + match fetch_and_update_leetcode(pool.clone(), member.member_id, &username).await + { + Ok(_) => { + println!("LeetCode stats updated for member ID: {}", member.member_id) + } Err(e) => eprintln!( - "Failed to update LeetCode stats for member ID {}: {:?}", + "Failed to update LeetCode stats for member ID {}: {:?}", member.member_id, e ), } @@ -109,11 +112,19 @@ pub async fn update_leaderboard_task(pool: Arc) { let username = codeforces_stats.codeforces_handle.clone(); // Fetch and update Codeforces stats - match fetch_and_update_codeforces_stats(pool.clone(), member.member_id, &username).await + match fetch_and_update_codeforces_stats( + pool.clone(), + member.member_id, + &username, + ) + .await { - Ok(_) => println!("Codeforces stats updated for member ID: {}", member.member_id), + Ok(_) => println!( + "Codeforces stats updated for member ID: {}", + member.member_id + ), Err(e) => eprintln!( - "Failed to update Codeforces stats for member ID {}: {:?}", + "Failed to update Codeforces stats for member ID {}: {:?}", member.member_id, e ), } @@ -130,8 +141,6 @@ pub async fn update_leaderboard_task(pool: Arc) { } } - - async fn update_attendance(members: Vec, pool: &PgPool) { #[allow(deprecated)] let today = chrono::Utc::now() @@ -208,4 +217,4 @@ async fn update_status_history(members: &Vec, pool: &PgPool) { } } } -} \ No newline at end of file +} diff --git a/src/graphql/mod.rs b/src/graphql/mod.rs index d81397b..b8bfc24 100644 --- a/src/graphql/mod.rs +++ b/src/graphql/mod.rs @@ -1,6 +1,11 @@ use async_graphql::MergedObject; -use mutations::{AttendanceMutations, MemberMutations, ProjectMutations, StreakMutations,FetchLeetCode,FetchCodeForces,LeaderboardMutation}; -use queries::{AttendanceQueries, MemberQueries, ProjectQueries, StreakQueries, LeaderboardQueries}; +use mutations::{ + AttendanceMutations, FetchCodeForces, FetchLeetCode, LeaderboardMutation, MemberMutations, + ProjectMutations, StreakMutations, +}; +use queries::{ + AttendanceQueries, LeaderboardQueries, MemberQueries, ProjectQueries, StreakQueries, +}; pub mod mutations; pub mod queries; @@ -23,5 +28,4 @@ pub struct Mutation( FetchLeetCode, FetchCodeForces, LeaderboardMutation, - ); diff --git a/src/graphql/mutations/codeforces_status.rs b/src/graphql/mutations/codeforces_status.rs index 4c45c7e..ee6793f 100644 --- a/src/graphql/mutations/codeforces_status.rs +++ b/src/graphql/mutations/codeforces_status.rs @@ -1,7 +1,7 @@ +use super::leaderboard_api::fetch_and_update_codeforces_stats; use async_graphql::{Context, Object, Result}; use sqlx::PgPool; use std::sync::Arc; -use super::leaderboard_api::fetch_and_update_codeforces_stats; #[derive(Default)] pub struct FetchCodeForces; diff --git a/src/graphql/mutations/leaderboard_api.rs b/src/graphql/mutations/leaderboard_api.rs index 1286762..b1edb14 100644 --- a/src/graphql/mutations/leaderboard_api.rs +++ b/src/graphql/mutations/leaderboard_api.rs @@ -1,16 +1,15 @@ use reqwest; +use reqwest::Client; use serde_json::Value; use sqlx::PgPool; -use std::sync::Arc; use std::collections::HashMap; -use reqwest::Client; - +use std::sync::Arc; pub async fn fetch_and_update_codeforces_stats( pool: Arc, member_id: i32, username: &str, -) -> Result<(), Box>{ +) -> 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)?; @@ -87,7 +86,11 @@ pub async fn update_leaderboard_scores(pool: Arc) -> Result<(), sqlx::Er .map(|row| { ( row.member_id, - (row.codeforces_rating, row.max_rating, row.contests_participated), + ( + row.codeforces_rating, + row.max_rating, + row.contests_participated, + ), ) }) .collect(); @@ -99,7 +102,8 @@ pub async fn update_leaderboard_scores(pool: Arc) -> Result<(), sqlx::Er + (2 * row.contests_participated) + (100 - row.best_rank / 10).max(0); - let (codeforces_score, unified_score) = cf_lookup.get(&row.member_id) + let (codeforces_score, unified_score) = cf_lookup + .get(&row.member_id) .map(|(rating, max_rating, contests)| { let cf_score = (rating / 10) + (max_rating / 20) + (5 * contests); (cf_score, leetcode_score + cf_score) @@ -123,18 +127,23 @@ pub async fn update_leaderboard_scores(pool: Arc) -> Result<(), sqlx::Er .await; if let Err(e) = result { - eprintln!("Failed to update leaderboard for member ID {}: {:?}", row.member_id, e); + eprintln!( + "Failed to update leaderboard for member ID {}: {:?}", + row.member_id, e + ); } } for row in &codeforces_stats { - if leetcode_stats.iter().any(|lc| lc.member_id == row.member_id) { + if leetcode_stats + .iter() + .any(|lc| lc.member_id == row.member_id) + { continue; } - let codeforces_score = (row.codeforces_rating / 10) - + (row.max_rating / 20) - + (5 * row.contests_participated); + let codeforces_score = + (row.codeforces_rating / 10) + (row.max_rating / 20) + (5 * row.contests_participated); let unified_score = codeforces_score; @@ -155,15 +164,16 @@ pub async fn update_leaderboard_scores(pool: Arc) -> Result<(), sqlx::Er .await; if let Err(e) = result { - eprintln!("Failed to update leaderboard for Codeforces-only member ID {}: {:?}", row.member_id, e); + eprintln!( + "Failed to update leaderboard for Codeforces-only member ID {}: {:?}", + row.member_id, e + ); } } Ok(()) } - - pub async fn fetch_and_update_leetcode( pool: Arc, member_id: i32, diff --git a/src/graphql/mutations/leetcode_status.rs b/src/graphql/mutations/leetcode_status.rs index 6c7e69b..39a05ea 100644 --- a/src/graphql/mutations/leetcode_status.rs +++ b/src/graphql/mutations/leetcode_status.rs @@ -1,7 +1,7 @@ +use super::leaderboard_api::fetch_and_update_leetcode; use async_graphql::{Context, Object, Result}; use sqlx::PgPool; use std::sync::Arc; -use super::leaderboard_api::fetch_and_update_leetcode; #[derive(Default)] pub struct FetchLeetCode; diff --git a/src/graphql/mutations/mod.rs b/src/graphql/mutations/mod.rs index 2bbe32a..20c5e85 100644 --- a/src/graphql/mutations/mod.rs +++ b/src/graphql/mutations/mod.rs @@ -1,23 +1,21 @@ pub mod attendance_mutations; +pub mod codeforces_status; +pub mod leaderboard_api; +pub mod leetcode_status; pub mod member_mutations; pub mod project_mutations; pub mod streak_mutations; -pub mod update_leaderboard; //leaderboard -pub mod leetcode_status; -pub mod codeforces_status; -pub mod leaderboard_api; - +pub mod update_leaderboard; //leaderboard pub use attendance_mutations::AttendanceMutations; -pub use member_mutations::MemberMutations; -pub use project_mutations::ProjectMutations; -pub use streak_mutations::StreakMutations; -pub use leetcode_status::FetchLeetCode; pub use codeforces_status::FetchCodeForces; -pub use update_leaderboard::LeaderboardMutation; pub use leaderboard_api::fetch_and_update_codeforces_stats; pub use leaderboard_api::fetch_and_update_leetcode; pub use leaderboard_api::update_leaderboard_scores; +pub use leetcode_status::FetchLeetCode; +pub use member_mutations::MemberMutations; +pub use project_mutations::ProjectMutations; +pub use streak_mutations::StreakMutations; +pub use update_leaderboard::LeaderboardMutation; - -//use any mutations for leaderboard if needed \ No newline at end of file +//use any mutations for leaderboard if needed diff --git a/src/graphql/mutations/update_leaderboard.rs b/src/graphql/mutations/update_leaderboard.rs index 3e57c0b..635f5d9 100644 --- a/src/graphql/mutations/update_leaderboard.rs +++ b/src/graphql/mutations/update_leaderboard.rs @@ -1,5 +1,5 @@ use async_graphql::{Context, Object, Result as GqlResult}; -use sqlx::{PgPool}; +use sqlx::PgPool; use std::collections::HashMap; use std::sync::Arc; @@ -9,10 +9,10 @@ pub struct LeaderboardMutation; #[Object] impl LeaderboardMutation { pub async fn update_leaderboard(&self, ctx: &Context<'_>) -> GqlResult { - let pool = ctx.data::>() + let pool = ctx + .data::>() .map_err(|_| async_graphql::Error::new("Failed to access the database pool"))?; - let leetcode_stats = sqlx::query!( "SELECT member_id, problems_solved, easy_solved, medium_solved, hard_solved, contests_participated, best_rank @@ -20,23 +20,30 @@ impl LeaderboardMutation { ) .fetch_all(pool.as_ref()) .await - .map_err(|e| async_graphql::Error::new(format!("Failed to fetch LeetCode stats: {:?}", e)))?; + .map_err(|e| { + async_graphql::Error::new(format!("Failed to fetch LeetCode stats: {:?}", e)) + })?; - let codeforces_stats = sqlx::query!( "SELECT member_id, codeforces_rating, max_rating, contests_participated FROM codeforces_stats" ) .fetch_all(pool.as_ref()) .await - .map_err(|e| async_graphql::Error::new(format!("Failed to fetch Codeforces stats: {:?}", e)))?; + .map_err(|e| { + async_graphql::Error::new(format!("Failed to fetch Codeforces stats: {:?}", e)) + })?; let cf_lookup: HashMap = codeforces_stats .iter() .map(|row| { ( row.member_id, - (row.codeforces_rating, row.max_rating, row.contests_participated), + ( + row.codeforces_rating, + row.max_rating, + row.contests_participated, + ), ) }) .collect(); @@ -48,7 +55,8 @@ impl LeaderboardMutation { + (2 * row.contests_participated) + (100 - row.best_rank / 10).max(0); - let (codeforces_score, unified_score) = cf_lookup.get(&row.member_id) + let (codeforces_score, unified_score) = cf_lookup + .get(&row.member_id) .map(|(rating, max_rating, contests)| { let cf_score = (rating / 10) + (max_rating / 20) + (5 * contests); (cf_score, leetcode_score + cf_score) @@ -72,12 +80,18 @@ impl LeaderboardMutation { .await; if let Err(e) = result { - eprintln!("Failed to update leaderboard for member ID {}: {:?}", row.member_id, e); + eprintln!( + "Failed to update leaderboard for member ID {}: {:?}", + row.member_id, e + ); } } for row in &codeforces_stats { - if leetcode_stats.iter().any(|lc| lc.member_id == row.member_id) { + if leetcode_stats + .iter() + .any(|lc| lc.member_id == row.member_id) + { continue; } @@ -96,7 +110,6 @@ impl LeaderboardMutation { unified_score = EXCLUDED.unified_score, last_updated = NOW()", row.member_id, - 0, codeforces_score, unified_score ) @@ -104,7 +117,10 @@ impl LeaderboardMutation { .await; if let Err(e) = result { - eprintln!("Failed to update leaderboard for Codeforces-only member ID {}: {:?}", row.member_id, e); + eprintln!( + "Failed to update leaderboard for Codeforces-only member ID {}: {:?}", + row.member_id, e + ); } } diff --git a/src/graphql/queries/leaderboard_queries.rs b/src/graphql/queries/leaderboard_queries.rs index ff25ff8..b3d6891 100644 --- a/src/graphql/queries/leaderboard_queries.rs +++ b/src/graphql/queries/leaderboard_queries.rs @@ -2,10 +2,12 @@ use async_graphql::{Context, Object}; use sqlx::PgPool; use std::sync::Arc; -use crate::models::leaderboard::{CodeforcesStatsWithName, LeaderboardWithMember, LeetCodeStatsWithName}; +use crate::models::leaderboard::{ + CodeforcesStatsWithName, LeaderboardWithMember, LeetCodeStatsWithName, +}; #[derive(Default)] -pub struct LeaderboardQueries; +pub struct LeaderboardQueries; #[Object] impl LeaderboardQueries { diff --git a/src/graphql/queries/mod.rs b/src/graphql/queries/mod.rs index e533c1c..0094344 100644 --- a/src/graphql/queries/mod.rs +++ b/src/graphql/queries/mod.rs @@ -1,12 +1,11 @@ pub mod attendance_queries; +pub mod leaderboard_queries; pub mod member_queries; pub mod project_queries; pub mod streak_queries; -pub mod leaderboard_queries; - pub use attendance_queries::AttendanceQueries; +pub use leaderboard_queries::LeaderboardQueries; pub use member_queries::MemberQueries; pub use project_queries::ProjectQueries; pub use streak_queries::StreakQueries; -pub use leaderboard_queries::LeaderboardQueries; diff --git a/src/models/leaderboard.rs b/src/models/leaderboard.rs index 7f9d915..e412c17 100644 --- a/src/models/leaderboard.rs +++ b/src/models/leaderboard.rs @@ -68,4 +68,4 @@ pub struct CodeforcesStatsWithName { pub codeforces_rating: i32, pub max_rating: i32, pub contests_participated: i32, -} \ No newline at end of file +} From 32fe3db74a3bab253de8c7ebfff03339ab5feb0a Mon Sep 17 00:00:00 2001 From: Aadarsh Date: Tue, 15 Jul 2025 18:55:49 +0530 Subject: [PATCH 10/10] fix: resolved failing workflows --- Cargo.lock | 38 ++++++--------------- src/daily_task/mod.rs | 8 ++--- src/graphql/mutations/leaderboard_api.rs | 13 ++++--- src/graphql/mutations/update_leaderboard.rs | 11 +++--- 4 files changed, 24 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 31ed16e..0c7d73c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,8 +76,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "036618f842229ba0b89652ffe425f96c7c16a49f7e3cb23b56fca7f61fd74980" dependencies = [ "async-graphql-derive", - "async-graphql-parser 7.0.15", - "async-graphql-value 7.0.15", + "async-graphql-parser", + "async-graphql-value", "async-stream", "async-trait", "base64 0.22.1", @@ -89,7 +89,7 @@ dependencies = [ "futures-util", "handlebars", "http", - "indexmap 2.7.1", + "indexmap", "mime", "multer", "num-traits", @@ -127,7 +127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd45deb3dbe5da5cdb8d6a670a7736d735ba65b455328440f236dfb113727a3d" dependencies = [ "Inflector", - "async-graphql-parser 7.0.15", + "async-graphql-parser", "darling", "proc-macro-crate", "proc-macro2", @@ -143,7 +143,7 @@ version = "7.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b7607e59424a35dadbc085b0d513aa54ec28160ee640cf79ec3b634eba66d3" dependencies = [ - "async-graphql-value 7.0.15", + "async-graphql-value", "pest", "serde", "serde_json", @@ -156,7 +156,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ecdaff7c9cffa3614a9f9999bf9ee4c3078fe3ce4d6a6e161736b56febf2de" dependencies = [ "bytes", - "indexmap 2.7.1", + "indexmap", "serde", "serde_json", ] @@ -892,7 +892,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.7.1", + "indexmap", "slab", "tokio", "tokio-util", @@ -913,12 +913,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.14.5" @@ -1271,17 +1265,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - [[package]] name = "indexmap" version = "2.7.1" @@ -2027,7 +2010,6 @@ version = "0.1.0" dependencies = [ "async-graphql", "async-graphql-axum", - "async-graphql-parser 2.11.3", "axum", "chrono", "chrono-tz", @@ -2381,8 +2363,8 @@ dependencies = [ "futures-io", "futures-util", "hashbrown 0.15.2", - "hashlink 0.10.0", - "indexmap 2.7.1", + "hashlink", + "indexmap", "log", "memchr", "once_cell", @@ -2891,7 +2873,7 @@ version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ - "indexmap 2.7.1", + "indexmap", "serde", "serde_spanned", "toml_datetime", diff --git a/src/daily_task/mod.rs b/src/daily_task/mod.rs index 0bc4c5a..29a636f 100644 --- a/src/daily_task/mod.rs +++ b/src/daily_task/mod.rs @@ -1,7 +1,7 @@ -use chrono::NaiveTime; use crate::graphql::mutations::{ fetch_and_update_codeforces_stats, fetch_and_update_leetcode, update_leaderboard_scores, }; +use chrono::NaiveTime; use chrono_tz::Asia::Kolkata; use sqlx::PgPool; use std::sync::Arc; @@ -133,15 +133,15 @@ pub async fn update_leaderboard_task(pool: Arc) { // Update leaderboard match update_leaderboard_scores(pool.clone()).await { Ok(_) => println!("Leaderboard updated."), - Err(e) => eprintln!("Failed to update leaderboard: {:?}", e), + Err(e) => eprintln!("Failed to update leaderboard: {e:?}"), } } } - Err(e) => eprintln!("Failed to fetch members: {:?}", e), + Err(e) => eprintln!("Failed to fetch members: {e:?}"), } } -async fn update_attendance(members: Vec, pool: &PgPool) { +async fn update_attendance(members: &Vec, pool: &PgPool) { #[allow(deprecated)] let today = chrono::Utc::now() .with_timezone(&Kolkata) diff --git a/src/graphql/mutations/leaderboard_api.rs b/src/graphql/mutations/leaderboard_api.rs index b1edb14..631c04b 100644 --- a/src/graphql/mutations/leaderboard_api.rs +++ b/src/graphql/mutations/leaderboard_api.rs @@ -10,7 +10,7 @@ pub async fn fetch_and_update_codeforces_stats( member_id: i32, username: &str, ) -> Result<(), Box> { - let url = format!("https://codeforces.com/api/user.rating?handle={}", username); + 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)?; @@ -51,18 +51,17 @@ pub async fn fetch_and_update_codeforces_stats( .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 - ), + 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()) + Err(format!("Failed to fetch stats for Codeforces handle: {username}").into()) } pub async fn update_leaderboard_scores(pool: Arc) -> Result<(), sqlx::Error> { diff --git a/src/graphql/mutations/update_leaderboard.rs b/src/graphql/mutations/update_leaderboard.rs index 635f5d9..cb774bc 100644 --- a/src/graphql/mutations/update_leaderboard.rs +++ b/src/graphql/mutations/update_leaderboard.rs @@ -20,9 +20,7 @@ impl LeaderboardMutation { ) .fetch_all(pool.as_ref()) .await - .map_err(|e| { - async_graphql::Error::new(format!("Failed to fetch LeetCode stats: {:?}", e)) - })?; + .map_err(|e| async_graphql::Error::new(format!("Failed to fetch LeetCode stats: {e:?}")))?; let codeforces_stats = sqlx::query!( "SELECT member_id, codeforces_rating, max_rating, contests_participated @@ -31,7 +29,7 @@ impl LeaderboardMutation { .fetch_all(pool.as_ref()) .await .map_err(|e| { - async_graphql::Error::new(format!("Failed to fetch Codeforces stats: {:?}", e)) + async_graphql::Error::new(format!("Failed to fetch Codeforces stats: {e:?}")) })?; let cf_lookup: HashMap = codeforces_stats @@ -72,8 +70,7 @@ impl LeaderboardMutation { unified_score = EXCLUDED.unified_score, last_updated = NOW()", row.member_id, - leetcode_score, - codeforces_score, + 0,codeforces_score, unified_score ) .execute(pool.as_ref()) @@ -110,7 +107,7 @@ impl LeaderboardMutation { unified_score = EXCLUDED.unified_score, last_updated = NOW()", row.member_id, - codeforces_score, + 0,codeforces_score, unified_score ) .execute(pool.as_ref())