diff --git a/migrations/20250913111615_missions.up.sql b/migrations/20250913111615_missions.up.sql index 5b199d5..8517e23 100644 --- a/migrations/20250913111615_missions.up.sql +++ b/migrations/20250913111615_missions.up.sql @@ -2,6 +2,8 @@ CREATE TABLE IF NOT EXISTS missions ( id BIGSERIAL PRIMARY KEY NOT NULL, name TEXT NOT NULL, description TEXT NOT NULL, + starting_at BIGINT NOT NULL, + estimated_length BIGINT NOT NULL, created_at BIGINT NOT NULL, modified_at BIGINT NOT NULL ); diff --git a/migrations/20250913130129_attendance.down.sql b/migrations/20250913130129_attendance.down.sql new file mode 100644 index 0000000..ebde145 --- /dev/null +++ b/migrations/20250913130129_attendance.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS attendance; diff --git a/migrations/20250913130129_attendance.up.sql b/migrations/20250913130129_attendance.up.sql new file mode 100644 index 0000000..eb7d586 --- /dev/null +++ b/migrations/20250913130129_attendance.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS attendance ( + id BIGSERIAL PRIMARY KEY NOT NULL, + user_id BIGINT NOT NULL, + mission_id BIGINT NOT NULL, + confirmed_at BIGINT NOT NULL, + attending BOOL NOT NULL, + + CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL, + CONSTRAINT fk_mission FOREIGN KEY (mission_id) REFERENCES missions(id) ON DELETE CASCADE +); diff --git a/src/api/user/mod.rs b/src/api/user/mod.rs index 215439f..32dc97c 100644 --- a/src/api/user/mod.rs +++ b/src/api/user/mod.rs @@ -9,7 +9,10 @@ pub mod login; pub mod register; pub fn register_routes() -> Router { + let sub_methods = Router::new(); + // .route("/awards", method_router); Router::new() .route("/register", post(register::route)) .route("/login", post(login::route)) + .nest("/{id}", sub_methods) } diff --git a/src/db/mod.rs b/src/db/mod.rs index b2ae9e2..8f5d9b7 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -3,7 +3,8 @@ use sqlx::{Pool, Postgres, postgres::PgPoolOptions}; pub mod tables; -pub type CurrPool = Pool; +pub type CurrDb = Postgres; +pub type CurrPool = Pool; #[derive(Debug, Clone)] pub struct Database { diff --git a/src/db/tables/assignables/awards.rs b/src/db/tables/assignables/awards.rs index 90f614a..6e27cbf 100644 --- a/src/db/tables/assignables/awards.rs +++ b/src/db/tables/assignables/awards.rs @@ -1,8 +1,9 @@ use anyhow::Result; +use sqlx::prelude::FromRow; -use crate::db::CurrPool; +use crate::db::{CurrPool, tables::TableMeta}; -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, FromRow)] pub struct Award { pub id: i64, pub name: String, @@ -11,6 +12,11 @@ pub struct Award { pub modified_at: i64, } +impl TableMeta<'_> for Award { + type PrimaryKey = i64; + const TABLE: &'static str = "awards"; +} + impl Award { pub async fn insert_new(&self, pool: &CurrPool) -> Result { let session = sqlx::query_as!( diff --git a/src/db/tables/assignables/missions.rs b/src/db/tables/assignables/missions.rs index 994ea55..3201262 100644 --- a/src/db/tables/assignables/missions.rs +++ b/src/db/tables/assignables/missions.rs @@ -1,16 +1,24 @@ use anyhow::Result; +use sqlx::prelude::FromRow; -use crate::db::CurrPool; +use crate::db::{CurrPool, tables::TableMeta}; -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, FromRow)] pub struct Mission { pub id: i64, pub name: String, pub description: String, + pub starting_at: i64, + pub estimated_length: i64, pub created_at: i64, pub modified_at: i64, } +impl TableMeta<'_> for Mission { + type PrimaryKey = i64; + const TABLE: &'static str = "missions"; +} + impl Mission { pub async fn insert_new(&self, pool: &CurrPool) -> Result { let session = sqlx::query_as!( diff --git a/src/db/tables/assignables/qualifications.rs b/src/db/tables/assignables/qualifications.rs index a141007..47cb92a 100644 --- a/src/db/tables/assignables/qualifications.rs +++ b/src/db/tables/assignables/qualifications.rs @@ -1,8 +1,9 @@ use anyhow::Result; +use sqlx::prelude::FromRow; -use crate::db::CurrPool; +use crate::db::{CurrPool, tables::TableMeta}; -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, FromRow)] pub struct Qualification { pub id: i64, pub name: String, @@ -11,6 +12,11 @@ pub struct Qualification { pub modified_at: i64, } +impl TableMeta<'_> for Qualification { + type PrimaryKey = i64; + const TABLE: &'static str = "qualifications"; +} + impl Qualification { pub async fn insert_new(&self, pool: &CurrPool) -> Result { let session = sqlx::query_as!( diff --git a/src/db/tables/assignables/ranks.rs b/src/db/tables/assignables/ranks.rs index 8d07d16..e6553a6 100644 --- a/src/db/tables/assignables/ranks.rs +++ b/src/db/tables/assignables/ranks.rs @@ -1,8 +1,9 @@ use anyhow::Result; +use sqlx::prelude::FromRow; -use crate::db::CurrPool; +use crate::db::{CurrPool, tables::TableMeta}; -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, FromRow)] pub struct Rank { pub id: i64, pub name: String, @@ -11,6 +12,11 @@ pub struct Rank { pub modified_at: i64, } +impl TableMeta<'_> for Rank { + type PrimaryKey = i64; + const TABLE: &'static str = "ranks"; +} + impl Rank { pub async fn insert_new(&self, pool: &CurrPool) -> Result { let session = sqlx::query_as!( diff --git a/src/db/tables/assignables/trainings.rs b/src/db/tables/assignables/trainings.rs index 7cdb889..ccccca6 100644 --- a/src/db/tables/assignables/trainings.rs +++ b/src/db/tables/assignables/trainings.rs @@ -1,8 +1,9 @@ use anyhow::Result; +use sqlx::prelude::FromRow; -use crate::db::CurrPool; +use crate::db::{CurrPool, tables::TableMeta}; -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, FromRow)] pub struct Training { pub id: i64, pub name: String, @@ -11,6 +12,11 @@ pub struct Training { pub modified_at: i64, } +impl TableMeta<'_> for Training { + type PrimaryKey = i64; + const TABLE: &'static str = "trainings"; +} + impl Training { pub async fn insert_new(&self, pool: &CurrPool) -> Result { let session = sqlx::query_as!( diff --git a/src/db/tables/attendance.rs b/src/db/tables/attendance.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/db/tables/mod.rs b/src/db/tables/mod.rs index f16be4c..103c804 100644 --- a/src/db/tables/mod.rs +++ b/src/db/tables/mod.rs @@ -1,4 +1,82 @@ +use std::{ + error::Error, + marker::PhantomData, + ops::{Deref, DerefMut}, +}; + +use sqlx::{Decode, Encode, QueryBuilder, Type, postgres::PgRow}; + pub mod assignables; +pub mod attendance; pub mod records; pub mod sessions; pub mod user; + +pub trait TableMeta<'a>: for<'r> sqlx::FromRow<'r, PgRow> { + type PrimaryKey: Encode<'a, super::CurrDb> + Type + Clone; + const TABLE: &'static str; +} + +#[derive(Debug, Clone, Default)] +pub struct ForeignKey<'a, FK: TableMeta<'a>> { + id: FK::PrimaryKey, + _ft: PhantomData, +} + +impl<'a, FK: TableMeta<'a> + Send + for<'r> sqlx::FromRow<'r, PgRow> + Unpin + 'a> + ForeignKey<'a, FK> +{ + pub async fn extract(&self, db: &super::CurrPool) -> anyhow::Result { + let id = self.id.clone(); + + let res = QueryBuilder::new("") + .push(format!("SELECT * FROM {} WHERE id = ", FK::TABLE)) + .push_bind(id) + .build_query_as::() + .fetch_one(db) + .await?; + + Ok(res) + } +} + +impl<'a, TM: TableMeta<'a>> Type for ForeignKey<'a, TM> { + fn type_info() -> ::TypeInfo { + TM::PrimaryKey::type_info() + } +} + +impl<'a, TM: TableMeta<'a>> Decode<'a, super::CurrDb> for ForeignKey<'a, TM> +where + &'a str: Decode<'a, super::CurrDb>, + >::PrimaryKey: sqlx::Decode<'a, super::CurrDb>, +{ + fn decode( + value: ::ValueRef<'a>, + ) -> Result> { + >::decode(value); + todo!() + } +} + +impl<'a, TM: TableMeta<'a>> Deref for ForeignKey<'a, TM> { + type Target = TM::PrimaryKey; + fn deref(&self) -> &Self::Target { + &self.id + } +} + +impl<'a, TM: TableMeta<'a>> DerefMut for ForeignKey<'a, TM> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.id + } +} + +impl<'a, FK: TableMeta<'a, PrimaryKey = i64>> From for ForeignKey<'a, FK> { + fn from(value: FK::PrimaryKey) -> Self { + Self { + id: value, + _ft: PhantomData, + } + } +} diff --git a/src/db/tables/records/awards.rs b/src/db/tables/records/awards.rs index b7b22ac..f53f7a9 100644 --- a/src/db/tables/records/awards.rs +++ b/src/db/tables/records/awards.rs @@ -1,17 +1,26 @@ use anyhow::Result; +use sqlx::prelude::FromRow; -use crate::db::CurrPool; +use crate::db::{ + CurrPool, + tables::{ForeignKey, TableMeta, assignables::awards::Award, user::User}, +}; -#[derive(Debug, Default, Clone)] -pub struct AwardRecord { +#[derive(Debug, Default, Clone, FromRow)] +pub struct AwardRecord<'a> { pub id: i64, - pub user_id: i64, - pub award_id: i64, - pub author_id: i64, + pub user_id: ForeignKey<'a, User>, + pub award_id: ForeignKey<'a, Award>, + pub author_id: ForeignKey<'a, User>, pub created_at: i64, } -impl AwardRecord { +impl<'a> TableMeta<'a> for AwardRecord<'a> { + type PrimaryKey = i64; + const TABLE: &'static str = "records_awards"; +} + +impl AwardRecord<'_> { pub async fn insert_new(&self, pool: &CurrPool) -> Result { let session = sqlx::query_as!( AwardRecord, @@ -20,14 +29,16 @@ impl AwardRecord { VALUES ($1, $2, $3, $4) RETURNING * "#, - self.user_id, - self.award_id, - self.author_id, + *self.user_id, + *self.award_id, + *self.author_id, self.created_at, ) .fetch_one(pool) .await?; + self.author_id.extract(pool); + Ok(session) } pub async fn get_by_id(pool: &CurrPool, id: i64) -> anyhow::Result { diff --git a/src/db/tables/records/missions.rs b/src/db/tables/records/missions.rs index 5b0ecda..e30c562 100644 --- a/src/db/tables/records/missions.rs +++ b/src/db/tables/records/missions.rs @@ -1,17 +1,26 @@ use anyhow::Result; +use sqlx::prelude::FromRow; -use crate::db::CurrPool; +use crate::db::{ + CurrPool, + tables::{ForeignKey, TableMeta, assignables::missions::Mission, user::User}, +}; -#[derive(Debug, Default, Clone)] -pub struct MissionRecord { +#[derive(Debug, Default, Clone, FromRow)] +pub struct MissionRecord<'a> { pub id: i64, - pub user_id: i64, - pub mission_id: i64, - pub author_id: i64, + pub user_id: ForeignKey<'a, User>, + pub mission_id: ForeignKey<'a, Mission>, + pub author_id: ForeignKey<'a, User>, pub created_at: i64, } -impl MissionRecord { +impl TableMeta<'_> for MissionRecord<'_> { + type PrimaryKey = i64; + const TABLE: &'static str = "records_missions"; +} + +impl MissionRecord<'_> { pub async fn insert_new(&self, pool: &CurrPool) -> Result { let session = sqlx::query_as!( MissionRecord, @@ -20,9 +29,9 @@ impl MissionRecord { VALUES ($1, $2, $3, $4) RETURNING * "#, - self.user_id, - self.mission_id, - self.author_id, + *self.user_id, + *self.mission_id, + *self.author_id, self.created_at, ) .fetch_one(pool) diff --git a/src/db/tables/records/qualifications.rs b/src/db/tables/records/qualifications.rs index 6a0b83a..4d09f2e 100644 --- a/src/db/tables/records/qualifications.rs +++ b/src/db/tables/records/qualifications.rs @@ -1,17 +1,26 @@ use anyhow::Result; +use sqlx::prelude::FromRow; -use crate::db::CurrPool; +use crate::db::{ + CurrPool, + tables::{ForeignKey, TableMeta, assignables::qualifications::Qualification, user::User}, +}; -#[derive(Debug, Default, Clone)] -pub struct QualificationRecord { +#[derive(Debug, Default, Clone, FromRow)] +pub struct QualificationRecord<'a> { pub id: i64, - pub user_id: i64, - pub qualification_id: i64, - pub author_id: i64, + pub user_id: ForeignKey<'a, User>, + pub author_id: ForeignKey<'a, User>, + pub qualification_id: ForeignKey<'a, Qualification>, pub created_at: i64, } -impl QualificationRecord { +impl<'a> TableMeta<'a> for QualificationRecord<'a> { + type PrimaryKey = i64; + const TABLE: &'static str = "records_qualifications"; +} + +impl<'a> QualificationRecord<'a> { pub async fn insert_new(&self, pool: &CurrPool) -> Result { let session = sqlx::query_as!( QualificationRecord, @@ -20,9 +29,9 @@ impl QualificationRecord { VALUES ($1, $2, $3, $4) RETURNING * "#, - self.user_id, - self.qualification_id, - self.author_id, + *self.user_id, + *self.qualification_id, + *self.author_id, self.created_at, ) .fetch_one(pool) diff --git a/src/db/tables/records/ranks.rs b/src/db/tables/records/ranks.rs index 28ae48b..8222643 100644 --- a/src/db/tables/records/ranks.rs +++ b/src/db/tables/records/ranks.rs @@ -1,17 +1,26 @@ use anyhow::Result; +use sqlx::prelude::FromRow; -use crate::db::CurrPool; +use crate::db::{ + CurrPool, + tables::{ForeignKey, TableMeta, assignables::ranks::Rank, user::User}, +}; -#[derive(Debug, Default, Clone)] -pub struct RankRecord { +#[derive(Debug, Default, Clone, FromRow)] +pub struct RankRecord<'a> { pub id: i64, - pub user_id: i64, - pub rank_id: i64, - pub author_id: i64, + pub user_id: ForeignKey<'a, User>, + pub rank_id: ForeignKey<'a, Rank>, + pub author_id: ForeignKey<'a, User>, pub created_at: i64, } -impl RankRecord { +impl<'a> TableMeta<'a> for RankRecord<'a> { + type PrimaryKey = i64; + const TABLE: &'static str = "records_ranks"; +} + +impl<'a> RankRecord<'a> { pub async fn insert_new(&self, pool: &CurrPool) -> Result { let session = sqlx::query_as!( RankRecord, @@ -20,9 +29,9 @@ impl RankRecord { VALUES ($1, $2, $3, $4) RETURNING * "#, - self.user_id, - self.rank_id, - self.author_id, + *self.user_id, + *self.rank_id, + *self.author_id, self.created_at, ) .fetch_one(pool) diff --git a/src/db/tables/records/trainings.rs b/src/db/tables/records/trainings.rs index 62eabbd..efb5462 100644 --- a/src/db/tables/records/trainings.rs +++ b/src/db/tables/records/trainings.rs @@ -1,17 +1,26 @@ use anyhow::Result; +use sqlx::prelude::FromRow; -use crate::db::CurrPool; +use crate::db::{ + CurrPool, + tables::{ForeignKey, TableMeta, assignables::trainings::Training, user::User}, +}; -#[derive(Debug, Default, Clone)] -pub struct TrainingRecord { +#[derive(Debug, Default, Clone, FromRow)] +pub struct TrainingRecord<'a> { pub id: i64, - pub user_id: i64, - pub training_id: i64, - pub author_id: i64, + pub user_id: ForeignKey<'a, User>, + pub training_id: ForeignKey<'a, Training>, + pub author_id: ForeignKey<'a, User>, pub created_at: i64, } -impl TrainingRecord { +impl<'a> TableMeta<'a> for TrainingRecord<'a> { + type PrimaryKey = i64; + const TABLE: &'static str = "records_trainings"; +} + +impl<'a> TrainingRecord<'a> { pub async fn insert_new(&self, pool: &CurrPool) -> Result { let session = sqlx::query_as!( TrainingRecord, @@ -20,9 +29,9 @@ impl TrainingRecord { VALUES ($1, $2, $3, $4) RETURNING * "#, - self.user_id, - self.training_id, - self.author_id, + *self.user_id, + *self.training_id, + *self.author_id, self.created_at, ) .fetch_one(pool) diff --git a/src/db/tables/sessions.rs b/src/db/tables/sessions.rs index f0417cf..a3f2a55 100644 --- a/src/db/tables/sessions.rs +++ b/src/db/tables/sessions.rs @@ -1,14 +1,20 @@ use anyhow::Result; +use sqlx::prelude::FromRow; -use crate::db::CurrPool; +use crate::db::{CurrPool, tables::TableMeta}; -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, FromRow)] pub struct Session { pub user_id: i64, pub session_key: String, pub expires: i64, } +impl TableMeta<'_> for Session { + type PrimaryKey = i64; + const TABLE: &'static str = "sessions"; +} + impl Session { pub async fn insert_new(&self, pool: &CurrPool) -> Result { let session = sqlx::query_as!( diff --git a/src/db/tables/user.rs b/src/db/tables/user.rs index 7b73810..b6a76bf 100644 --- a/src/db/tables/user.rs +++ b/src/db/tables/user.rs @@ -1,8 +1,8 @@ use anyhow::bail; +use sqlx::prelude::FromRow; -use crate::db::CurrPool; - -#[derive(Debug, Default, Clone)] +use crate::db::{CurrPool, tables::TableMeta}; +#[derive(Debug, Default, Clone, FromRow)] pub struct User { pub id: i64, pub email: String, @@ -11,6 +11,12 @@ pub struct User { pub pw_hash: String, pub pw_salt: String, pub pfp_id: i64, + pub rank_id: i64, +} + +impl TableMeta<'_> for User { + type PrimaryKey = i64; + const TABLE: &'static str = "users"; } impl User {