diff --git a/migrations/20260810073000_create_member_life_status.sql b/migrations/20260810073000_create_member_life_status.sql new file mode 100644 index 0000000..cb39aac --- /dev/null +++ b/migrations/20260810073000_create_member_life_status.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS MemberLifeStatus ( + member_id INT REFERENCES Member(member_id) ON DELETE CASCADE PRIMARY KEY, + lives INT NOT NULL DEFAULT 3, + recovery_streak INT NOT NULL DEFAULT 0, + is_probation BOOLEAN NOT NULL DEFAULT FALSE, + last_reset_month INT NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/src/graphql/mutations/status_mutations.rs b/src/graphql/mutations/status_mutations.rs index ca49399..aacd8ca 100644 --- a/src/graphql/mutations/status_mutations.rs +++ b/src/graphql/mutations/status_mutations.rs @@ -1,14 +1,25 @@ -use async_graphql::{Context, Object, Result}; +use async_graphql::{Context, InputObject, Object, Result}; use chrono::NaiveDate; use sqlx::PgPool; use std::sync::Arc; use crate::auth::guards::{AdminGuard, AdminOrBotGuard}; -use crate::models::status_update::{CreateStatusBreakInput, StatusBreakRecord, StatusUpdateRecord}; +use crate::models::status_update::{ + CreateStatusBreakInput, MemberLifeStatusRecord, StatusBreakRecord, StatusUpdateRecord, +}; #[derive(Default)] pub struct StatusMutations; +#[derive(InputObject)] +struct UpdateLifeStatusInput { + member_id: i32, + lives: i32, + recovery_streak: i32, + is_probation: bool, + last_reset_month: i32, +} + #[Object] impl StatusMutations { #[graphql(name = "markStatusUpdate", guard = "AdminOrBotGuard")] @@ -36,6 +47,44 @@ impl StatusMutations { Ok(status) } + #[graphql(name = "updateLifeStatus", guard = "AdminOrBotGuard")] + async fn update_life_status( + &self, + ctx: &Context<'_>, + input: UpdateLifeStatusInput, + ) -> Result { + let pool = ctx.data::>().expect("Pool must be in context"); + + if input.lives < 0 || input.lives > 3 { + return Err("lives must be between 0 and 3".into()); + } + if input.recovery_streak < 0 || input.recovery_streak > 3 { + return Err("recovery_streak must be between 0 and 3".into()); + } + + let record = sqlx::query_as::<_, MemberLifeStatusRecord>( + "INSERT INTO MemberLifeStatus + (member_id, lives, recovery_streak, is_probation, last_reset_month, updated_at) + VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP) + ON CONFLICT (member_id) DO UPDATE SET + lives = EXCLUDED.lives, + recovery_streak = EXCLUDED.recovery_streak, + is_probation = EXCLUDED.is_probation, + last_reset_month = EXCLUDED.last_reset_month, + updated_at = CURRENT_TIMESTAMP + RETURNING member_id, lives, recovery_streak, is_probation, last_reset_month", + ) + .bind(input.member_id) + .bind(input.lives) + .bind(input.recovery_streak) + .bind(input.is_probation) + .bind(input.last_reset_month) + .fetch_one(pool.as_ref()) + .await?; + + Ok(record) + } + #[graphql(name = "createStatusBreak", guard = "AdminGuard")] async fn create_status_break( &self, diff --git a/src/graphql/queries/member_queries.rs b/src/graphql/queries/member_queries.rs index c15e107..c0dea73 100644 --- a/src/graphql/queries/member_queries.rs +++ b/src/graphql/queries/member_queries.rs @@ -8,6 +8,7 @@ use chrono::NaiveDate; use sqlx::PgPool; use std::sync::Arc; +use crate::models::status_update::MemberLifeStatusRecord; use crate::models::{member::Member, status_update::StatusUpdateStreakRecord}; #[derive(Default)] @@ -262,6 +263,18 @@ impl StatusInfo { Ok(result) } + + async fn life_status(&self, ctx: &Context<'_>) -> Result> { + let pool = ctx.data::>()?; + let status = sqlx::query_as::<_, MemberLifeStatusRecord>( + "SELECT member_id, lives, recovery_streak, is_probation, last_reset_month + FROM MemberLifeStatus WHERE member_id = $1", + ) + .bind(self.member_id) + .fetch_optional(pool.as_ref()) + .await?; + Ok(status) + } } #[ComplexObject] diff --git a/src/models/status_update.rs b/src/models/status_update.rs index 7691b3d..c12bb96 100644 --- a/src/models/status_update.rs +++ b/src/models/status_update.rs @@ -35,3 +35,12 @@ pub struct CreateStatusBreakInput { pub member_id: Option, pub reason: Option, } + +#[derive(SimpleObject, FromRow, Debug, Clone, PartialEq, Eq)] +pub struct MemberLifeStatusRecord { + pub member_id: i32, + pub lives: i32, + pub recovery_streak: i32, + pub is_probation: bool, + pub last_reset_month: i32, +}