From 6614f5a6adf3780553d6ebba55361ad913a6c438 Mon Sep 17 00:00:00 2001 From: Joel Klinghed Date: Sat, 28 Dec 2024 10:40:20 +0100 Subject: Database connection --- server/src/api_model.rs | 64 +++++++---- server/src/main.rs | 284 +++++++++++++++++++++++++++++++++++++----------- 2 files changed, 264 insertions(+), 84 deletions(-) (limited to 'server/src') diff --git a/server/src/api_model.rs b/server/src/api_model.rs index 4b6eced..c5b1ce3 100644 --- a/server/src/api_model.rs +++ b/server/src/api_model.rs @@ -2,45 +2,69 @@ use serde::{Serialize}; #[derive(Serialize, Copy, Clone)] pub enum ReviewState { - DRAFT, - OPEN, - DROPPED, - CLOSED, + Draft, + Open, + Dropped, + Closed, } #[derive(Serialize)] -pub struct User<'r> { - pub username: &'r str, - pub name: &'r str, +pub struct User { + pub id: u64, + pub username: String, + pub name: String, pub active: bool, } #[derive(Serialize)] -pub struct Review<'r> { +pub struct Review { pub id: u64, - pub title: &'r str, - pub description: &'r str, - pub owner: &'r User<'r>, - pub reviewers: Vec<&'r User<'r>>, - pub watchers: Vec<&'r User<'r>>, + pub title: String, + pub description: String, + pub owner: User, + pub reviewers: Vec, + pub watchers: Vec, pub state: ReviewState, - pub progress: f64, + pub progress: f32, } #[derive(Serialize)] -pub struct ReviewEntry<'r> { +pub struct ReviewEntry { pub id: u64, - pub title: &'r str, - pub owner: &'r User<'r>, + pub title: String, + pub owner: User, pub state: ReviewState, - pub progress: f64, + pub progress: f32, +} + +#[derive(Serialize)] +pub struct Reviews { + pub offset: u32, + pub limit: u32, + pub total_count: u32, + pub more: bool, + pub reviews: Vec, +} + +#[derive(Serialize)] +pub struct Project { + pub id: u64, + pub title: String, + pub description: String, + pub members: Vec, +} + +#[derive(Serialize)] +pub struct ProjectEntry { + pub id: u64, + pub title: String, } #[derive(Serialize)] -pub struct Reviews<'r> { +pub struct Projects { pub offset: u32, pub limit: u32, pub total_count: u32, pub more: bool, - pub reviews: Vec>, + pub projects: Vec, } diff --git a/server/src/main.rs b/server/src/main.rs index 702d954..9f7204a 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,54 +1,19 @@ #[macro_use] extern crate rocket; +use futures::{stream::TryStreamExt, future::TryFutureExt}; + +use rocket::{Rocket, Build, futures}; +use rocket::fairing::{self, AdHoc}; use rocket::request::{self, Outcome, Request, FromRequest}; use rocket::response::{status::NotFound}; use rocket::serde::json::Json; +use rocket_db_pools::{sqlx, Database, Connection}; mod api_model; -static USER1: api_model::User = api_model::User { - username: "u1", - name: "User #1", - active: true, -}; - -static USER2: api_model::User = api_model::User { - username: "u2", - name: "User #2", - active: true, -}; - -static USER3: api_model::User = api_model::User { - username: "u3", - name: "User #3", - active: true, -}; - -fn make_r1<'r>() -> api_model::Review<'r> { - api_model::Review { - id: 1, - title: "Review #1", - description: "Description for review #1", - owner: &USER1, - reviewers: vec![&USER2, &USER3], - watchers: vec![], - state: api_model::ReviewState::OPEN, - progress: 0.42, - } -} - -fn make_r2<'r>() -> api_model::Review<'r> { - api_model::Review { - id: 2, - title: "Review #2", - description: "Description for review #2", - owner: &USER2, - reviewers: vec![&USER1], - watchers: vec![&USER3], - state: api_model::ReviewState::OPEN, - progress: 0.9999, - } -} +#[derive(Database)] +#[database("eyeballs")] +struct Db(sqlx::MySqlPool); struct User { username: String, @@ -69,42 +34,233 @@ impl<'r> FromRequest<'r> for User { } } -fn review_entry<'r>(review: &api_model::Review<'r>) -> api_model::ReviewEntry<'r> { - api_model::ReviewEntry { - id: review.id, - title: review.title, - owner: review.owner, - state: review.state, - progress: review.progress, +enum Role { + Reviewer, + Watcher, +} + +struct UserRole { + user: api_model::User, + role: Role, +} + +impl TryFrom for Role { + type Error = &'static str; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Role::Reviewer), + 1 => Ok(Role::Watcher), + _ => Err("Invalid role") + } + } +} + +impl TryFrom for api_model::ReviewState { + type Error = &'static str; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(api_model::ReviewState::Draft), + 1 => Ok(api_model::ReviewState::Open), + 2 => Ok(api_model::ReviewState::Dropped), + 3 => Ok(api_model::ReviewState::Closed), + _ => Err("Invalid review state") + } } } -#[get("/reviews")] -async fn reviews<'r>(_user: User) -> Json> { +#[get("/projects?&")] +async fn projects<'r>(mut db: Connection, _user: User, limit: Option, offset: Option) -> Json { + let uw_offset = offset.unwrap_or(0); + let uw_limit = limit.unwrap_or(10); + + let entries = sqlx::query!( + "SELECT id,title FROM projects ORDER BY title,id LIMIT ? OFFSET ?", + uw_limit, uw_offset) + .fetch(&mut **db) + .map_ok(|r| api_model::ProjectEntry { + id: r.id, + title: r.title, + }) + .try_collect::>() + .await + .unwrap(); + + let count = sqlx::query!( + "SELECT COUNT(id) AS count FROM projects") + .fetch_one(&mut **db) + .map_ok(|r| r.count) + .await + .unwrap(); + + let u32_count = u32::try_from(count).unwrap(); + + Json( + api_model::Projects { + offset: uw_offset, + limit: uw_limit, + total_count: u32_count, + more: uw_offset + uw_limit < u32_count, + projects: entries, + }, + ) +} + +#[get("/project/")] +async fn project<'r>(mut db: Connection, _user: User, projectid: u64) -> Result, NotFound> { + let members = sqlx::query!( + "SELECT id, username, name, active FROM users JOIN project_users ON project_users.user=users.id WHERE project_users.project=?", + projectid) + .fetch(&mut **db) + .map_ok(|r| api_model::User { + id: r.id, + username: r.username, + name: r.name, + active: r.active != 0, + }) + .try_collect::>() + .await + .unwrap(); + + let project = sqlx::query!( + "SELECT id,title,description FROM projects WHERE id=?", + projectid) + .fetch_one(&mut **db) + .map_ok(|r| api_model::Project { + id: r.id, + title: r.title, + description: r.description, + members: members, + }) + .await + .map_err(|e| NotFound(e.to_string())) + .unwrap(); + + Ok(Json(project)) +} + +#[get("/reviews/?&")] +async fn reviews<'r>(mut db: Connection, _user: User, projectid: u64, limit: Option, offset: Option) -> Json { + let uw_offset = offset.unwrap_or(0); + let uw_limit = limit.unwrap_or(10); + let entries = sqlx::query!( + "SELECT reviews.id AS id,title,state,progress,users.id AS user_id,users.username AS username,users.name AS name,users.active AS user_active FROM reviews JOIN users ON users.id=owner WHERE project=? ORDER BY id DESC LIMIT ? OFFSET ?", + projectid, uw_limit, uw_offset) + .fetch(&mut **db) + .map_ok(|r| api_model::ReviewEntry { + id: r.id, + title: r.title, + owner: api_model::User { + id: r.user_id, + username: r.username, + name: r.name, + active: r.user_active != 0, + }, + state: api_model::ReviewState::try_from(r.state).unwrap(), + progress: r.progress, + }) + .try_collect::>() + .await + .unwrap(); + + let count = sqlx::query!( + "SELECT COUNT(id) AS count FROM reviews WHERE project=?", + projectid) + .fetch_one(&mut **db) + .map_ok(|r| r.count) + .await + .unwrap(); + + let u32_count = u32::try_from(count).unwrap(); + Json( api_model::Reviews { - offset: 0, - limit: 10, - total_count: 2, - more: false, - reviews: vec![review_entry(&make_r1()), review_entry(&make_r2())], + offset: uw_offset, + limit: uw_limit, + total_count: u32_count, + more: uw_offset + uw_limit < u32_count, + reviews: entries, }, ) } -#[get("/review/")] -async fn review<'r>(id: u64, _user: User) -> Result>, NotFound> { - match id { - 1 => Ok(Json(make_r1())), - 2 => Ok(Json(make_r2())), - _ => Err(NotFound(id.to_string())) +#[get("/review//")] +async fn review<'r>(mut db: Connection, _user: User, projectid: u64, reviewid: u64) -> Result, NotFound> { + let mut users = sqlx::query!( + "SELECT id,username,name,active,review_users.role AS role FROM users JOIN review_users ON review_users.user=id WHERE review_users.review=? ORDER BY role,username,id", + reviewid) + .fetch(&mut **db) + .map_ok(|r| UserRole { + user: api_model::User { + id: r.id, + username: r.username, + name: r.name, + active: r.active != 0, + }, + role: Role::try_from(r.role).unwrap(), + }) + .try_collect::>() + .await + .unwrap(); + + let first_reviewer = users.iter() + .position(|u| matches!(u.role, Role::Reviewer)) + .unwrap_or(users.len()); + let mut reviewers: Vec = Vec::with_capacity(first_reviewer); + for user_role in users.drain(0..first_reviewer) { + reviewers.push(user_role.user); + } + let mut watchers: Vec = Vec::with_capacity(users.len()); + for user_role in users.drain(0..) { + watchers.push(user_role.user); + } + + let review = sqlx::query!( + "SELECT reviews.id AS id,title,description,state,progress,users.id AS user_id,users.username AS username,users.name AS name,users.active AS user_active FROM reviews JOIN users ON users.id=owner WHERE project=? AND reviews.id=?", + projectid, reviewid) + .fetch_one(&mut **db) + .map_ok(|r| api_model::Review { + id: r.id, + title: r.title, + description: r.description, + owner: api_model::User { + id: r.user_id, + username: r.username, + name: r.name, + active: r.user_active != 0, + }, + reviewers: reviewers, + watchers: watchers, + state: api_model::ReviewState::try_from(r.state).unwrap(), + progress: r.progress, + }) + .await + .map_err(|e| NotFound(e.to_string())) + .unwrap(); + + Ok(Json(review)) +} + +async fn run_migrations(rocket: Rocket) -> fairing::Result { + match Db::fetch(&rocket) { + Some(db) => match sqlx::migrate!("./migrations").run(&**db).await { + Ok(_) => Ok(rocket), + Err(e) => { + error!("Failed to initialize database: {}", e); + Err(rocket) + } + } + None => Err(rocket), } } #[rocket::main] async fn main() -> Result<(), rocket::Error> { let _rocket = rocket::build() - .mount("/api/v1", routes![reviews, review]) + .attach(Db::init()) + .attach(AdHoc::try_on_ignite("Database Migrations", run_migrations)) + .mount("/api/v1", routes![projects, project, reviews, review]) .launch() .await?; -- cgit v1.2.3-70-g09d2