From cc60ff4827fbdcde3a528b6d759f82c013263ba5 Mon Sep 17 00:00:00 2001 From: Leandro Date: Tue, 5 Mar 2024 21:51:11 -0300 Subject: [PATCH 01/98] feat: CRUD users, organizations and repositories --- .gitignore | 3 +- db.sql | 27 +- docker-compose.yaml | 15 +- src/contributions/db.rs | 65 ---- src/contributions/handlers.rs | 64 ---- src/contributions/models.rs | 24 -- src/contributions/routes.rs | 47 --- src/handlers.rs | 8 +- src/main.rs | 12 +- src/organization/db.rs | 80 +++++ src/{contributions => organization}/errors.rs | 24 +- src/organization/handlers.rs | 64 ++++ src/{contributions => organization}/mod.rs | 0 src/organization/models.rs | 27 ++ src/organization/routes.rs | 47 +++ src/repository/db.rs | 91 ++++++ src/repository/errors.rs | 46 +++ src/repository/handlers.rs | 71 +++++ src/repository/mod.rs | 5 + src/repository/models.rs | 31 ++ src/repository/routes.rs | 49 +++ src/tests/contribution.rs | 298 ++++++++++-------- src/user/db.rs | 72 +++++ src/user/errors.rs | 46 +++ src/user/handlers.rs | 45 +++ src/user/mod.rs | 5 + src/user/models.rs | 27 ++ src/user/routes.rs | 44 +++ 28 files changed, 983 insertions(+), 354 deletions(-) delete mode 100644 src/contributions/db.rs delete mode 100644 src/contributions/handlers.rs delete mode 100644 src/contributions/models.rs delete mode 100644 src/contributions/routes.rs create mode 100644 src/organization/db.rs rename src/{contributions => organization}/errors.rs (54%) create mode 100644 src/organization/handlers.rs rename src/{contributions => organization}/mod.rs (100%) create mode 100644 src/organization/models.rs create mode 100644 src/organization/routes.rs create mode 100644 src/repository/db.rs create mode 100644 src/repository/errors.rs create mode 100644 src/repository/handlers.rs create mode 100644 src/repository/mod.rs create mode 100644 src/repository/models.rs create mode 100644 src/repository/routes.rs create mode 100644 src/user/db.rs create mode 100644 src/user/errors.rs create mode 100644 src/user/handlers.rs create mode 100644 src/user/mod.rs create mode 100644 src/user/models.rs create mode 100644 src/user/routes.rs diff --git a/.gitignore b/.gitignore index c41cc9e..ccb5166 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/target \ No newline at end of file +/target +.vscode \ No newline at end of file diff --git a/db.sql b/db.sql index caa91ca..b7e3289 100644 --- a/db.sql +++ b/db.sql @@ -1,5 +1,24 @@ -CREATE TABLE IF NOT EXISTS contribution -( - id bigint PRIMARY KEY NOT NULL, - created_at timestamp with time zone DEFAULT (now() at time zone 'utc') +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + username VARCHAR(100) UNIQUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') +); +CREATE TABLE IF NOT EXISTS organizations ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) UNIQUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') +); +CREATE TABLE IF NOT EXISTS repositories ( + id SERIAL PRIMARY KEY, + name VARCHAR(255), + organization_id INT REFERENCES organizations(id), + -- user_id INT REFERENCES users(id), // TODO: allow users + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') +); +CREATE TABLE IF NOT EXISTS issues ( + id SERIAL PRIMARY KEY, + issue_number INT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), + updated_at TIMESTAMP, + repository_id INT REFERENCES repositories(id) -- TODO: add "tipping" status ); \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index bacdf3e..5533797 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,7 @@ -version: '3.8' +version: "3.8" services: api: - image: dontelmo/kudos_api + # image: dontelmo/kudos_api/ networks: - kudos build: @@ -27,13 +27,12 @@ services: POSTGRES_DB: database POSTGRES_USER: postgres POSTGRES_PASSWORD: password - volumes: - - pgdata:/var/lib/postgresql/data - -volumes: - pgdata: +# volumes: +# - pgdata:/var/lib/postgresql/data +# volumes: +# pgdata: networks: kudos: - driver: bridge \ No newline at end of file + driver: bridge diff --git a/src/contributions/db.rs b/src/contributions/db.rs deleted file mode 100644 index c381f83..0000000 --- a/src/contributions/db.rs +++ /dev/null @@ -1,65 +0,0 @@ -use mobc::async_trait; -use mobc_postgres::tokio_postgres::Row; -use warp::reject; - -use crate::db::{ - pool::DBAccess, - utils::{ - execute_query_with_timeout, query_one_timeout, query_opt_timeout, query_with_timeout, - DB_QUERY_TIMEOUT, - }, -}; - -use super::models::{Contribution, ContributionRequest}; - -const TABLE: &str = "contribution"; - -#[async_trait] -pub trait DBContribution: Send + Sync + Clone + 'static { - async fn get_contribution(&self, id: i64) -> Result, reject::Rejection>; - async fn get_contributions(&self) -> Result, reject::Rejection>; - async fn create_contribution( - &self, - contribution: ContributionRequest, - ) -> Result; - async fn delete_contribution(&self, id: i64) -> Result<(), reject::Rejection>; -} - -#[async_trait] -impl DBContribution for DBAccess { - async fn get_contribution(&self, id: i64) -> Result, reject::Rejection> { - let query = format!( - "SELECT id FROM {} WHERE id = $1 ORDER BY created_at DESC", - TABLE - ); - match query_opt_timeout(self, query.as_str(), &[&id], DB_QUERY_TIMEOUT).await? { - Some(contribution) => Ok(Some(row_to_contribution(&contribution))), - None => Ok(None), - } - } - - async fn get_contributions(&self) -> Result, reject::Rejection> { - let query = format!("SELECT id FROM {} ORDER BY created_at DESC", TABLE); - let rows = query_with_timeout(self, query.as_str(), &[], DB_QUERY_TIMEOUT).await?; - Ok(rows.iter().map(row_to_contribution).collect()) - } - - async fn create_contribution( - &self, - contribution: ContributionRequest, - ) -> Result { - let query = format!("INSERT INTO {} (id) VALUES ($1) RETURNING *", TABLE); - let row = query_one_timeout(self, &query, &[&contribution.id], DB_QUERY_TIMEOUT).await?; - Ok(row_to_contribution(&row)) - } - - async fn delete_contribution(&self, id: i64) -> Result<(), reject::Rejection> { - let query = format!("DELETE FROM {} WHERE id = $1", TABLE); - execute_query_with_timeout(self, &query, &[&id], DB_QUERY_TIMEOUT).await - } -} - -fn row_to_contribution(row: &Row) -> Contribution { - let id: i64 = row.get(0); - Contribution { id } -} diff --git a/src/contributions/handlers.rs b/src/contributions/handlers.rs deleted file mode 100644 index c9293b5..0000000 --- a/src/contributions/handlers.rs +++ /dev/null @@ -1,64 +0,0 @@ -use warp::{ - http::StatusCode, - reject::Rejection, - reply::{json, Reply}, -}; - -use super::{ - db::DBContribution, - errors::ContributionError, - models::{ContributionRequest, ContributionResponse}, -}; - -pub async fn create_contribution_handler( - body: ContributionRequest, - db_access: impl DBContribution, -) -> Result { - match db_access.get_contribution(body.id).await? { - Some(_) => Err(warp::reject::custom(ContributionError::ContributionExists( - body.id, - )))?, - None => Ok(json(&ContributionResponse::of( - db_access.create_contribution(body).await?, - ))), - } -} - -pub async fn get_contribution_handler( - id: i64, - db_access: impl DBContribution, -) -> Result { - match db_access.get_contribution(id).await? { - None => Err(warp::reject::custom( - ContributionError::ContributionNotFound(id), - ))?, - Some(contribution) => Ok(json(&ContributionResponse::of(contribution))), - } -} - -pub async fn get_contributions_handler( - db_access: impl DBContribution, -) -> Result { - let contributions = db_access.get_contributions().await?; - Ok(json::>( - &contributions - .into_iter() - .map(ContributionResponse::of) - .collect(), - )) -} - -pub async fn delete_contribution_handler( - id: i64, - db_access: impl DBContribution, -) -> Result { - match db_access.get_contribution(id).await? { - Some(_) => { - let _ = &db_access.delete_contribution(id).await?; - Ok(StatusCode::OK) - } - None => Err(warp::reject::custom( - ContributionError::ContributionNotFound(id), - ))?, - } -} diff --git a/src/contributions/models.rs b/src/contributions/models.rs deleted file mode 100644 index 146756c..0000000 --- a/src/contributions/models.rs +++ /dev/null @@ -1,24 +0,0 @@ -use serde_derive::{Deserialize, Serialize}; - -#[derive(Deserialize)] -pub struct Contribution { - pub id: i64, -} - -#[derive(Serialize, Deserialize)] -pub struct ContributionRequest { - pub id: i64, -} - -#[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct ContributionResponse { - pub id: i64, -} - -impl ContributionResponse { - pub fn of(contribution: Contribution) -> ContributionResponse { - ContributionResponse { - id: contribution.id, - } - } -} diff --git a/src/contributions/routes.rs b/src/contributions/routes.rs deleted file mode 100644 index 672d245..0000000 --- a/src/contributions/routes.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::convert::Infallible; - -use warp::filters::BoxedFilter; -use warp::{Filter, Reply}; - -use super::db::DBContribution; -use super::handlers; - -fn with_db( - db_pool: impl DBContribution, -) -> impl Filter + Clone { - warp::any().map(move || db_pool.clone()) -} - -pub fn routes(db_access: impl DBContribution) -> BoxedFilter<(impl Reply,)> { - let contribution = warp::path!("contribution"); - let contribution_id = warp::path!("contribution" / i64); - - let get_contributions = contribution - .and(warp::get()) - .and(with_db(db_access.clone())) - .and_then(handlers::get_contributions_handler); - - let get_contribution = contribution_id - .and(warp::get()) - // .and(warp::path::param()) - .and(with_db(db_access.clone())) - .and_then(handlers::get_contribution_handler); - - let create_contribution = contribution - .and(warp::post()) - .and(warp::body::json()) - .and(with_db(db_access.clone())) - .and_then(handlers::create_contribution_handler); - - let delete_contribution = contribution_id - .and(warp::delete()) - .and(with_db(db_access.clone())) - .and_then(handlers::delete_contribution_handler); - - let route = get_contributions - .or(get_contribution) - .or(create_contribution) - .or(delete_contribution); - - route.boxed() -} diff --git a/src/handlers.rs b/src/handlers.rs index f29f91e..652659c 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -3,7 +3,9 @@ use serde_derive::Deserialize; use std::convert::Infallible; use warp::{hyper::StatusCode, Rejection, Reply}; -use crate::{contributions::errors::ContributionError, db::errors::DBError}; +use crate::{ + db::errors::DBError, organization::errors::OrganizationError, user::errors::UserError, +}; #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct ErrorResponse { @@ -11,7 +13,9 @@ pub struct ErrorResponse { } pub async fn error_handler(err: Rejection) -> std::result::Result { - if let Some(e) = err.find::() { + if let Some(e) = err.find::() { + Ok(e.clone().into_response()) + } else if let Some(e) = err.find::() { Ok(e.clone().into_response()) } else if let Some(e) = err.find::() { let (code, message) = match e { diff --git a/src/main.rs b/src/main.rs index 7ff3875..bcfd515 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,10 +5,12 @@ use warp::Filter; use crate::types::ApiConfig; -mod contributions; mod db; mod handlers; mod health; +mod organization; +mod repository; +mod user; #[cfg(test)] mod tests; @@ -30,12 +32,16 @@ async fn run() { let db = init_db(database_url, database_init_file).await.unwrap(); //If there's an error the api should panic let health_route = health::routes::routes(db.clone()); - let contribution_route = contributions::routes::routes(db); + let users_route = user::routes::routes(db.clone()); + let organizations_route = organization::routes::routes(db.clone()); + let repositories_route = repository::routes::routes(db); let error_handler = handlers::error_handler; // string all the routes together let routes = health_route - .or(contribution_route) + .or(users_route) + .or(organizations_route) + .or(repositories_route) .with(warp::cors().allow_any_origin()) .recover(error_handler); diff --git a/src/organization/db.rs b/src/organization/db.rs new file mode 100644 index 0000000..481b56c --- /dev/null +++ b/src/organization/db.rs @@ -0,0 +1,80 @@ +use mobc::async_trait; +use mobc_postgres::tokio_postgres::Row; +use warp::reject; + +use crate::db::{ + pool::DBAccess, + utils::{ + execute_query_with_timeout, query_one_timeout, query_opt_timeout, query_with_timeout, + DB_QUERY_TIMEOUT, + }, +}; + +use super::models::{Organization, OrganizationRequest}; + +const TABLE: &str = "organizations"; + +#[async_trait] +pub trait DBOrganization: Send + Sync + Clone + 'static { + async fn get_organization(&self, id: i32) -> Result, reject::Rejection>; + async fn get_organization_by_name( + &self, + name: &str, + ) -> Result, reject::Rejection>; + async fn get_organizations(&self) -> Result, reject::Rejection>; + async fn create_organization( + &self, + organization: OrganizationRequest, + ) -> Result; + async fn delete_organization(&self, id: i32) -> Result<(), reject::Rejection>; +} + +#[async_trait] +impl DBOrganization for DBAccess { + async fn get_organization(&self, id: i32) -> Result, reject::Rejection> { + let query = format!("SELECT * FROM {} WHERE id = $1", TABLE); + match query_opt_timeout(self, query.as_str(), &[&id], DB_QUERY_TIMEOUT).await? { + Some(organization) => Ok(Some(row_to_organization(&organization))), + None => Ok(None), + } + } + async fn get_organization_by_name( + &self, + name: &str, + ) -> Result, reject::Rejection> { + let query = format!("SELECT * FROM {} WHERE name = $1", TABLE); + match query_opt_timeout(self, query.as_str(), &[&name], DB_QUERY_TIMEOUT).await? { + Some(organization) => Ok(Some(row_to_organization(&organization))), + None => Ok(None), + } + } + + async fn get_organizations(&self) -> Result, reject::Rejection> { + let query = format!("SELECT * FROM {} ORDER BY created_at DESC", TABLE); + let rows = query_with_timeout(self, query.as_str(), &[], DB_QUERY_TIMEOUT).await?; + Ok(rows.iter().map(row_to_organization).collect()) + } + + async fn create_organization( + &self, + organization: OrganizationRequest, + ) -> Result { + let query = format!("INSERT INTO {} (name) VALUES ($1) RETURNING *", TABLE); + let row = query_one_timeout(self, &query, &[&organization.name], DB_QUERY_TIMEOUT).await?; + Ok(row_to_organization(&row)) + } + + async fn delete_organization(&self, id: i32) -> Result<(), reject::Rejection> { + let query = format!("DELETE FROM {} WHERE id = $1", TABLE); + execute_query_with_timeout(self, &query, &[&id], DB_QUERY_TIMEOUT).await + } +} + +fn row_to_organization(row: &Row) -> Organization { + let id: i32 = row.get(0); + let name: &str = row.get(1); + Organization { + id, + name: name.to_string(), + } +} diff --git a/src/contributions/errors.rs b/src/organization/errors.rs similarity index 54% rename from src/contributions/errors.rs rename to src/organization/errors.rs index daa9021..fe4c209 100644 --- a/src/contributions/errors.rs +++ b/src/organization/errors.rs @@ -11,31 +11,31 @@ use warp::{ use crate::handlers::ErrorResponse; #[derive(Clone, Error, Debug, Deserialize, PartialEq)] -pub enum ContributionError { - ContributionExists(i64), - ContributionNotFound(i64), +pub enum OrganizationError { + OrganizationExists(i32), + OrganizationNotFound(i32), } -impl fmt::Display for ContributionError { +impl fmt::Display for OrganizationError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ContributionError::ContributionExists(id) => { - write!(f, "Contribution #{} already exists", id) + OrganizationError::OrganizationExists(id) => { + write!(f, "Organization #{} already exists", id) } - ContributionError::ContributionNotFound(id) => { - write!(f, "Contribution #{} not found", id) + OrganizationError::OrganizationNotFound(id) => { + write!(f, "Organization #{} not found", id) } } } } -impl Reject for ContributionError {} +impl Reject for OrganizationError {} -impl Reply for ContributionError { +impl Reply for OrganizationError { fn into_response(self) -> Response { let code = match self { - ContributionError::ContributionExists(_) => StatusCode::BAD_REQUEST, - ContributionError::ContributionNotFound(_) => StatusCode::NOT_FOUND, + OrganizationError::OrganizationExists(_) => StatusCode::BAD_REQUEST, + OrganizationError::OrganizationNotFound(_) => StatusCode::NOT_FOUND, }; let message = self.to_string(); diff --git a/src/organization/handlers.rs b/src/organization/handlers.rs new file mode 100644 index 0000000..8524594 --- /dev/null +++ b/src/organization/handlers.rs @@ -0,0 +1,64 @@ +use warp::{ + http::StatusCode, + reject::Rejection, + reply::{json, Reply}, +}; + +use super::{ + db::DBOrganization, + errors::OrganizationError, + models::{OrganizationRequest, OrganizationResponse}, +}; + +pub async fn create_organization_handler( + body: OrganizationRequest, + db_access: impl DBOrganization, +) -> Result { + match db_access.get_organization_by_name(&body.name).await? { + Some(u) => Err(warp::reject::custom(OrganizationError::OrganizationExists( + u.id, + )))?, + None => Ok(json(&OrganizationResponse::of( + db_access.create_organization(body).await?, + ))), + } +} + +pub async fn get_organization_handler( + id: i32, + db_access: impl DBOrganization, +) -> Result { + match db_access.get_organization(id).await? { + None => Err(warp::reject::custom( + OrganizationError::OrganizationNotFound(id), + ))?, + Some(organization) => Ok(json(&OrganizationResponse::of(organization))), + } +} + +pub async fn get_organizations_handler( + db_access: impl DBOrganization, +) -> Result { + let organizations = db_access.get_organizations().await?; + Ok(json::>( + &organizations + .into_iter() + .map(OrganizationResponse::of) + .collect(), + )) +} + +pub async fn delete_organization_handler( + id: i32, + db_access: impl DBOrganization, +) -> Result { + match db_access.get_organization(id).await? { + Some(_) => { + let _ = &db_access.delete_organization(id).await?; + Ok(StatusCode::OK) + } + None => Err(warp::reject::custom( + OrganizationError::OrganizationNotFound(id), + ))?, + } +} diff --git a/src/contributions/mod.rs b/src/organization/mod.rs similarity index 100% rename from src/contributions/mod.rs rename to src/organization/mod.rs diff --git a/src/organization/models.rs b/src/organization/models.rs new file mode 100644 index 0000000..43cd4c7 --- /dev/null +++ b/src/organization/models.rs @@ -0,0 +1,27 @@ +use serde_derive::{Deserialize, Serialize}; + +#[derive(Deserialize)] +pub struct Organization { + pub id: i32, + pub name: String, +} + +#[derive(Serialize, Deserialize)] +pub struct OrganizationRequest { + pub name: String, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct OrganizationResponse { + pub id: i32, + pub name: String, +} + +impl OrganizationResponse { + pub fn of(organization: Organization) -> OrganizationResponse { + OrganizationResponse { + id: organization.id, + name: organization.name, + } + } +} diff --git a/src/organization/routes.rs b/src/organization/routes.rs new file mode 100644 index 0000000..4d8d04b --- /dev/null +++ b/src/organization/routes.rs @@ -0,0 +1,47 @@ +use std::convert::Infallible; + +use warp::filters::BoxedFilter; +use warp::{Filter, Reply}; + +use super::db::DBOrganization; +use super::handlers; + +fn with_db( + db_pool: impl DBOrganization, +) -> impl Filter + Clone { + warp::any().map(move || db_pool.clone()) +} + +pub fn routes(db_access: impl DBOrganization) -> BoxedFilter<(impl Reply,)> { + let organization = warp::path!("organizations"); + let organization_id = warp::path!("organizations" / i32); + + let get_organizations = organization + .and(warp::get()) + .and(with_db(db_access.clone())) + .and_then(handlers::get_organizations_handler); + + let get_organization = organization_id + .and(warp::get()) + // .and(warp::path::param()) + .and(with_db(db_access.clone())) + .and_then(handlers::get_organization_handler); + + let create_organization = organization + .and(warp::post()) + .and(warp::body::json()) + .and(with_db(db_access.clone())) + .and_then(handlers::create_organization_handler); + + let delete_organization = organization_id + .and(warp::delete()) + .and(with_db(db_access.clone())) + .and_then(handlers::delete_organization_handler); + + let route = get_organizations + .or(get_organization) + .or(create_organization) + .or(delete_organization); + + route.boxed() +} diff --git a/src/repository/db.rs b/src/repository/db.rs new file mode 100644 index 0000000..eb6be95 --- /dev/null +++ b/src/repository/db.rs @@ -0,0 +1,91 @@ +use mobc::async_trait; +use mobc_postgres::tokio_postgres::Row; +use warp::reject; + +use crate::db::{ + pool::DBAccess, + utils::{ + execute_query_with_timeout, query_one_timeout, query_opt_timeout, query_with_timeout, + DB_QUERY_TIMEOUT, + }, +}; + +use super::models::{Repository, RepositoryRequest}; + +const TABLE: &str = "repositories"; + +#[async_trait] +pub trait DBRepository: Send + Sync + Clone + 'static { + async fn get_repository(&self, id: i32) -> Result, reject::Rejection>; + async fn get_repository_by_name( + &self, + name: &str, + ) -> Result, reject::Rejection>; + async fn get_repositories(&self) -> Result, reject::Rejection>; + async fn create_repository( + &self, + repository: RepositoryRequest, + ) -> Result; + async fn delete_repository(&self, id: i32) -> Result<(), reject::Rejection>; +} + +#[async_trait] +impl DBRepository for DBAccess { + async fn get_repository(&self, id: i32) -> Result, reject::Rejection> { + let query = format!("SELECT * FROM {} WHERE id = $1", TABLE); + match query_opt_timeout(self, query.as_str(), &[&id], DB_QUERY_TIMEOUT).await? { + Some(repository) => Ok(Some(row_to_repository(&repository))), + None => Ok(None), + } + } + async fn get_repository_by_name( + &self, + name: &str, + ) -> Result, reject::Rejection> { + let query = format!("SELECT * FROM {} WHERE name = $1", TABLE); + match query_opt_timeout(self, query.as_str(), &[&name], DB_QUERY_TIMEOUT).await? { + Some(repository) => Ok(Some(row_to_repository(&repository))), + None => Ok(None), + } + } + + async fn get_repositories(&self) -> Result, reject::Rejection> { + let query = format!("SELECT * FROM {} ORDER BY created_at DESC", TABLE); + let rows = query_with_timeout(self, query.as_str(), &[], DB_QUERY_TIMEOUT).await?; + Ok(rows.iter().map(row_to_repository).collect()) + } + + async fn create_repository( + &self, + repository: RepositoryRequest, + ) -> Result { + let query = format!( + "INSERT INTO {} (name, organization_id) VALUES ($1, $2) RETURNING *", + TABLE + ); + let row = query_one_timeout( + self, + &query, + &[&repository.name, &repository.organization_id], + DB_QUERY_TIMEOUT, + ) + .await?; + Ok(row_to_repository(&row)) + } + + async fn delete_repository(&self, id: i32) -> Result<(), reject::Rejection> { + let query = format!("DELETE FROM {} WHERE id = $1", TABLE); + execute_query_with_timeout(self, &query, &[&id], DB_QUERY_TIMEOUT).await + } +} + +fn row_to_repository(row: &Row) -> Repository { + let id: i32 = row.get(0); + let name: &str = row.get(1); + let organization_id: i32 = row.get(2); + Repository { + id, + name: name.to_string(), + organization_id, + } +} diff --git a/src/repository/errors.rs b/src/repository/errors.rs new file mode 100644 index 0000000..20f5d98 --- /dev/null +++ b/src/repository/errors.rs @@ -0,0 +1,46 @@ +use std::fmt; + +use serde_derive::Deserialize; +use thiserror::Error; +use warp::{ + http::StatusCode, + reject::Reject, + reply::{Reply, Response}, +}; + +use crate::handlers::ErrorResponse; + +#[derive(Clone, Error, Debug, Deserialize, PartialEq)] +pub enum RepositoryError { + RepositoryExists(i32), + RepositoryNotFound(i32), +} + +impl fmt::Display for RepositoryError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RepositoryError::RepositoryExists(id) => { + write!(f, "Repository #{} already exists", id) + } + RepositoryError::RepositoryNotFound(id) => { + write!(f, "Repository #{} not found", id) + } + } + } +} + +impl Reject for RepositoryError {} + +impl Reply for RepositoryError { + fn into_response(self) -> Response { + let code = match self { + RepositoryError::RepositoryExists(_) => StatusCode::BAD_REQUEST, + RepositoryError::RepositoryNotFound(_) => StatusCode::NOT_FOUND, + }; + let message = self.to_string(); + + let json = warp::reply::json(&ErrorResponse { message }); + + warp::reply::with_status(json, code).into_response() + } +} diff --git a/src/repository/handlers.rs b/src/repository/handlers.rs new file mode 100644 index 0000000..91e25bd --- /dev/null +++ b/src/repository/handlers.rs @@ -0,0 +1,71 @@ +use warp::{ + http::StatusCode, + reject::Rejection, + reply::{json, Reply}, +}; + +use crate::organization::{db::DBOrganization, errors::OrganizationError}; + +use super::{ + db::DBRepository, + errors::RepositoryError, + models::{RepositoryRequest, RepositoryResponse}, +}; + +pub async fn create_repository_handler( + body: RepositoryRequest, + db_access: impl DBRepository + DBOrganization, +) -> Result { + match db_access.get_organization(body.organization_id).await? { + Some(_) => match db_access.get_repository_by_name(&body.name).await? { + Some(u) => Err(warp::reject::custom(RepositoryError::RepositoryExists( + u.id, + )))?, + None => Ok(json(&RepositoryResponse::of( + db_access.create_repository(body).await?, + ))), + }, + None => Err(warp::reject::custom( + OrganizationError::OrganizationNotFound(body.organization_id), + ))?, + } +} + +pub async fn get_repository_handler( + id: i32, + db_access: impl DBRepository, +) -> Result { + match db_access.get_repository(id).await? { + None => Err(warp::reject::custom(RepositoryError::RepositoryNotFound( + id, + )))?, + Some(repository) => Ok(json(&RepositoryResponse::of(repository))), + } +} + +pub async fn get_repositories_handler( + db_access: impl DBRepository, +) -> Result { + let repositories = db_access.get_repositories().await?; + Ok(json::>( + &repositories + .into_iter() + .map(RepositoryResponse::of) + .collect(), + )) +} + +pub async fn delete_repository_handler( + id: i32, + db_access: impl DBRepository, +) -> Result { + match db_access.get_repository(id).await? { + Some(_) => { + let _ = &db_access.delete_repository(id).await?; + Ok(StatusCode::OK) + } + None => Err(warp::reject::custom(RepositoryError::RepositoryNotFound( + id, + )))?, + } +} diff --git a/src/repository/mod.rs b/src/repository/mod.rs new file mode 100644 index 0000000..93246e5 --- /dev/null +++ b/src/repository/mod.rs @@ -0,0 +1,5 @@ +pub mod db; +pub mod errors; +pub mod handlers; +pub mod models; +pub mod routes; diff --git a/src/repository/models.rs b/src/repository/models.rs new file mode 100644 index 0000000..3983468 --- /dev/null +++ b/src/repository/models.rs @@ -0,0 +1,31 @@ +use serde_derive::{Deserialize, Serialize}; + +#[derive(Deserialize)] +pub struct Repository { + pub id: i32, + pub name: String, + pub organization_id: i32, +} + +#[derive(Serialize, Deserialize)] +pub struct RepositoryRequest { + pub name: String, + pub organization_id: i32, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct RepositoryResponse { + pub id: i32, + pub name: String, + pub organization_id: i32, +} + +impl RepositoryResponse { + pub fn of(repository: Repository) -> RepositoryResponse { + RepositoryResponse { + id: repository.id, + name: repository.name, + organization_id: repository.organization_id, + } + } +} diff --git a/src/repository/routes.rs b/src/repository/routes.rs new file mode 100644 index 0000000..5b2a99e --- /dev/null +++ b/src/repository/routes.rs @@ -0,0 +1,49 @@ +use std::convert::Infallible; + +use warp::filters::BoxedFilter; +use warp::{Filter, Reply}; + +use crate::organization::db::DBOrganization; + +use super::db::DBRepository; +use super::handlers; + +fn with_db( + db_pool: impl DBRepository + DBOrganization, +) -> impl Filter + Clone { + warp::any().map(move || db_pool.clone()) +} + +pub fn routes(db_access: impl DBRepository + DBOrganization) -> BoxedFilter<(impl Reply,)> { + let repository = warp::path!("repositories"); // TODO: move this to the "organization" endpoint as a subendpoint + let repository_id = warp::path!("repositories" / i32); + + let get_repositories = repository + .and(warp::get()) + .and(with_db(db_access.clone())) + .and_then(handlers::get_repositories_handler); + + let get_repository = repository_id + .and(warp::get()) + // .and(warp::path::param()) + .and(with_db(db_access.clone())) + .and_then(handlers::get_repository_handler); + + let create_repository = repository + .and(warp::post()) + .and(warp::body::json()) + .and(with_db(db_access.clone())) + .and_then(handlers::create_repository_handler); + + let delete_repository = repository_id + .and(warp::delete()) + .and(with_db(db_access.clone())) + .and_then(handlers::delete_repository_handler); + + let route = get_repositories + .or(get_repository) + .or(create_repository) + .or(delete_repository); + + route.boxed() +} diff --git a/src/tests/contribution.rs b/src/tests/contribution.rs index 9eb7012..11b27e9 100644 --- a/src/tests/contribution.rs +++ b/src/tests/contribution.rs @@ -1,13 +1,13 @@ #[cfg(test)] mod tests { use crate::{ - contributions::routes::routes, - contributions::{ - db::DBContribution, - models::{Contribution, ContributionRequest, ContributionResponse}, - }, handlers::{error_handler, ErrorResponse}, init_db, + user::routes::routes, + user::{ + db::DBUser, + models::{User, UserRequest, UserResponse}, + }, }; use mobc::async_trait; use warp::{reject, test::request, Filter}; @@ -18,82 +18,90 @@ mod tests { pub struct DBMockEmpty {} #[async_trait] - impl DBContribution for DBMockValues { - async fn get_contribution( + impl DBUser for DBMockValues { + async fn get_user(&self, id: i32) -> Result, reject::Rejection> { + Ok(Some(User { + id, + username: "username".to_string(), + })) + } + async fn get_user_by_username( &self, - id: i64, - ) -> Result, reject::Rejection> { - Ok(Some(Contribution { id })) + username: &str, + ) -> Result, reject::Rejection> { + Ok(Some(User { + id: 1, + username: username.to_string(), + })) } - async fn get_contributions(&self) -> Result, reject::Rejection> { - Ok(vec![Contribution { id: 1 }]) + async fn get_users(&self) -> Result, reject::Rejection> { + Ok(vec![User { + id: 1, + username: "username".to_string(), + }]) } - async fn create_contribution( - &self, - contribution: ContributionRequest, - ) -> Result { - Ok(Contribution { - id: contribution.id, + async fn create_user(&self, user: UserRequest) -> Result { + Ok(User { + id: 1, + username: user.username, }) } - async fn delete_contribution(&self, _: i64) -> Result<(), reject::Rejection> { + async fn delete_user(&self, _: i32) -> Result<(), reject::Rejection> { Ok(()) } } #[async_trait] - impl DBContribution for DBMockEmpty { - async fn get_contribution( - &self, - _: i64, - ) -> Result, reject::Rejection> { + impl DBUser for DBMockEmpty { + async fn get_user(&self, _: i32) -> Result, reject::Rejection> { Ok(None) } - async fn get_contributions(&self) -> Result, reject::Rejection> { + async fn get_users(&self) -> Result, reject::Rejection> { Ok(vec![]) } - async fn create_contribution( + async fn get_user_by_username( &self, - contribution: ContributionRequest, - ) -> Result { - Ok(Contribution { - id: contribution.id, + _username: &str, + ) -> Result, reject::Rejection> { + Ok(None) + } + async fn create_user(&self, user: UserRequest) -> Result { + Ok(User { + id: 1, + username: user.username, }) } - async fn delete_contribution(&self, _: i64) -> Result<(), reject::Rejection> { + async fn delete_user(&self, _: i32) -> Result<(), reject::Rejection> { Ok(()) } } #[tokio::test] - async fn test_get_contribution_mock_db() { + async fn test_get_user_mock_db() { let id = 1; let r = routes(DBMockValues {}); - let resp = request() - .path(&format!("/contribution/{id}")) - .reply(&r) - .await; + let resp = request().path(&format!("/users/{id}")).reply(&r).await; assert_eq!(resp.status(), 200); let body = resp.into_body(); assert!(!body.is_empty()); - let expected_response = ContributionResponse { id }; - let response: ContributionResponse = serde_json::from_slice(&body).unwrap(); + let expected_response = UserResponse { + id, + username: "username".to_string(), + }; + let response: UserResponse = serde_json::from_slice(&body).unwrap(); assert_eq!(response, expected_response) } #[tokio::test] - async fn test_get_contribution_not_found_mock_db() { + async fn test_get_user_not_found_mock_db() { let id = 1; let r = routes(DBMockEmpty {}).recover(error_handler); - let resp = request() - .path(&format!("/contribution/{id}")) - .reply(&r) - .await; + let resp = request().path(&format!("/users/{id}")).reply(&r).await; assert_eq!(resp.status(), 404); let body = resp.into_body(); assert!(!body.is_empty()); let expected_response = ErrorResponse { - message: format!("Contribution #{} not found", id), + message: format!("User #{} not found", id), }; let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); @@ -101,38 +109,45 @@ mod tests { } #[tokio::test] - async fn test_get_contributions_mock_db() { + async fn test_get_users_mock_db() { let r = routes(DBMockValues {}); - let resp = request().path(&format!("/contribution")).reply(&r).await; + let resp = request().path(&format!("/users")).reply(&r).await; assert_eq!(resp.status(), 200); let body = resp.into_body(); assert!(!body.is_empty()); - let expected_response = vec![ContributionResponse { id: 1 }]; - let response: Vec = serde_json::from_slice(&body).unwrap(); + let expected_response = vec![UserResponse { + id: 1, + username: "username".to_string(), + }]; + let response: Vec = serde_json::from_slice(&body).unwrap(); assert_eq!(response, expected_response) } #[tokio::test] - async fn test_get_contributions_empty_mock_db() { + async fn test_get_users_empty_mock_db() { let r = routes(DBMockEmpty {}); - let resp = request().path(&format!("/contribution")).reply(&r).await; + let resp = request().path(&format!("/users")).reply(&r).await; assert_eq!(resp.status(), 200); let body = resp.into_body(); - let response: Vec = serde_json::from_slice(&body).unwrap(); - let expected_response: Vec = vec![]; + let response: Vec = serde_json::from_slice(&body).unwrap(); + let expected_response: Vec = vec![]; assert_eq!(response, expected_response); } #[tokio::test] - async fn test_create_contribution_mock_db() { + async fn test_create_user_mock_db() { let id = 1; - let new_contribution = serde_json::to_vec(&ContributionRequest { id }).unwrap(); + let new_user = serde_json::to_vec(&UserResponse { + id: 1, + username: "username".to_string(), + }) + .unwrap(); let r = routes(DBMockEmpty {}); let resp = request() - .body(new_contribution) - .path(&"/contribution") + .body(new_user) + .path(&"/users") .method("POST") .reply(&r) .await; @@ -140,19 +155,26 @@ mod tests { let body = resp.into_body(); assert!(!body.is_empty()); - let expected_response = ContributionResponse { id }; - let response: ContributionResponse = serde_json::from_slice(&body).unwrap(); + let expected_response = UserResponse { + id, + username: "username".to_string(), + }; + let response: UserResponse = serde_json::from_slice(&body).unwrap(); assert_eq!(response, expected_response) } #[tokio::test] - async fn test_create_contribution_already_exists_mock_db() { + async fn test_create_user_already_exists_mock_db() { let id = 1; - let new_contribution = serde_json::to_vec(&ContributionRequest { id }).unwrap(); + let new_user = serde_json::to_vec(&UserResponse { + id: 1, + username: "username".to_string(), + }) + .unwrap(); let r = routes(DBMockValues {}).recover(error_handler); let resp = request() - .body(new_contribution) - .path(&"/contribution") + .body(new_user) + .path(&"/users") .method("POST") .reply(&r) .await; @@ -161,7 +183,7 @@ mod tests { assert!(!body.is_empty()); let expected_response = ErrorResponse { - message: format!("Contribution #{} already exists", id), + message: format!("User #{} already exists", id), }; let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); @@ -169,11 +191,11 @@ mod tests { } #[tokio::test] - async fn test_delete_contribution_mock_db() { + async fn test_delete_user_mock_db() { let id = 1; let r = routes(DBMockValues {}); let resp = request() - .path(&format!("/contribution/{id}")) + .path(&format!("/users/{id}")) .method("DELETE") .reply(&r) .await; @@ -183,13 +205,17 @@ mod tests { } #[tokio::test] - async fn test_delete_contribution_does_not_exist_mock_db() { + async fn test_delete_user_does_not_exist_mock_db() { let id = 1; - let new_contribution = serde_json::to_vec(&ContributionRequest { id }).unwrap(); + let new_user = serde_json::to_vec(&UserResponse { + id: 1, + username: "username".to_string(), + }) + .unwrap(); let r = routes(DBMockEmpty {}).recover(error_handler); let resp = request() - .body(new_contribution) - .path(&format!("/contribution/{id}")) + .body(new_user) + .path(&format!("/users/{id}")) .method("DELETE") .reply(&r) .await; @@ -199,7 +225,7 @@ mod tests { assert!(!body.is_empty()); let expected_response = ErrorResponse { - message: format!("Contribution #{} not found", id), + message: format!("User #{} not found", id), }; let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); assert_eq!(response, expected_response) @@ -207,9 +233,13 @@ mod tests { #[tokio::test] #[ignore] - async fn test_create_contribution_db() { + async fn test_create_user_db() { let id = 1; - let new_contribution = serde_json::to_vec(&ContributionRequest { id }).unwrap(); + let new_user = serde_json::to_vec(&UserResponse { + id: 1, + username: "username".to_string(), + }) + .unwrap(); let db = init_db( "postgres://postgres:password@localhost:5432/database".to_string(), "db_test.sql".to_string(), @@ -218,8 +248,8 @@ mod tests { .unwrap(); let r = routes(db); let resp = request() - .body(new_contribution) - .path(&"/contribution") + .body(new_user) + .path(&"/users") .method("POST") .reply(&r) .await; @@ -227,16 +257,23 @@ mod tests { let body = resp.into_body(); assert!(!body.is_empty()); - let expected_response = ContributionResponse { id }; - let response: ContributionResponse = serde_json::from_slice(&body).unwrap(); + let expected_response = UserResponse { + id, + username: "username".to_string(), + }; + let response: UserResponse = serde_json::from_slice(&body).unwrap(); assert_eq!(response, expected_response) } #[tokio::test] #[ignore] - async fn test_create_contribution_already_exists_db() { + async fn test_create_user_already_exists_db() { let id = 1; - let new_contribution = serde_json::to_vec(&ContributionRequest { id }).unwrap(); + let new_user = serde_json::to_vec(&UserResponse { + id: 1, + username: "username".to_string(), + }) + .unwrap(); let db = init_db( "postgres://postgres:password@localhost:5432/database".to_string(), "db_test.sql".to_string(), @@ -245,14 +282,14 @@ mod tests { .unwrap(); let r = routes(db).recover(error_handler); let _ = request() - .body(new_contribution.clone()) - .path(&"/contribution") + .body(new_user.clone()) + .path(&"/users") .method("POST") .reply(&r) .await; let resp = request() - .body(new_contribution) - .path(&"/contribution") + .body(new_user) + .path(&"/users") .method("POST") .reply(&r) .await; @@ -261,7 +298,7 @@ mod tests { assert!(!body.is_empty()); let expected_response = ErrorResponse { - message: format!("Contribution #{} already exists", id), + message: format!("User #{} already exists", id), }; let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); @@ -270,7 +307,7 @@ mod tests { #[tokio::test] #[ignore] - async fn test_get_contribution_not_found_db() { + async fn test_get_user_not_found_db() { let id = 1; let db = init_db( "postgres://postgres:password@localhost:5432/database".to_string(), @@ -279,17 +316,14 @@ mod tests { .await .unwrap(); let r = routes(db).recover(error_handler); - let resp = request() - .path(&format!("/contribution/{id}")) - .reply(&r) - .await; + let resp = request().path(&format!("/users/{id}")).reply(&r).await; assert_eq!(resp.status(), 404); let body = resp.into_body(); assert!(!body.is_empty()); let expected_response = ErrorResponse { - message: "Contribution #1 not found".to_string(), + message: "User #1 not found".to_string(), }; let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); @@ -298,9 +332,13 @@ mod tests { #[tokio::test] #[ignore] - async fn test_get_contribution_db() { + async fn test_get_user_db() { let id = 1; - let new_contribution = serde_json::to_vec(&ContributionRequest { id }).unwrap(); + let new_user = serde_json::to_vec(&UserResponse { + id: 1, + username: "username".to_string(), + }) + .unwrap(); let db = init_db( "postgres://postgres:password@localhost:5432/database".to_string(), "db_test.sql".to_string(), @@ -309,26 +347,26 @@ mod tests { .unwrap(); let r = routes(db); let _ = request() - .body(new_contribution) - .path(&"/contribution") + .body(new_user) + .path(&"/users") .method("POST") .reply(&r) .await; - let resp = request() - .path(&format!("/contribution/{id}")) - .reply(&r) - .await; + let resp = request().path(&format!("/users/{id}")).reply(&r).await; // assert_eq!(resp.status(), 200); let body = resp.into_body(); assert!(!body.is_empty()); - let expected_response = ContributionResponse { id }; - let response: ContributionResponse = serde_json::from_slice(&body).unwrap(); + let expected_response = UserResponse { + id, + username: "username".to_string(), + }; + let response: UserResponse = serde_json::from_slice(&body).unwrap(); assert_eq!(response, expected_response) } #[tokio::test] #[ignore] - async fn test_get_contributions_empty_db() { + async fn test_get_users_empty_db() { let db = init_db( "postgres://postgres:password@localhost:5432/database".to_string(), "db_test.sql".to_string(), @@ -336,19 +374,23 @@ mod tests { .await .unwrap(); let r = routes(db).recover(error_handler); - let resp = request().path(&format!("/contribution")).reply(&r).await; + let resp = request().path(&format!("/users")).reply(&r).await; assert_eq!(resp.status(), 200); let body = resp.into_body(); - let response: Vec = serde_json::from_slice(&body).unwrap(); - let expected_response: Vec = vec![]; + let response: Vec = serde_json::from_slice(&body).unwrap(); + let expected_response: Vec = vec![]; assert_eq!(response, expected_response); } #[tokio::test] #[ignore] - async fn test_get_contributions_db() { + async fn test_get_users_db() { let id = 1; - let new_contribution = serde_json::to_vec(&ContributionRequest { id }).unwrap(); + let new_user = serde_json::to_vec(&UserResponse { + id: 1, + username: "username".to_string(), + }) + .unwrap(); let db = init_db( "postgres://postgres:password@localhost:5432/database".to_string(), "db_test.sql".to_string(), @@ -357,26 +399,33 @@ mod tests { .unwrap(); let r = routes(db); let _ = request() - .body(new_contribution) - .path(&"/contribution") + .body(new_user) + .path(&"/users") .method("POST") .reply(&r) .await; - let resp = request().path(&format!("/contribution")).reply(&r).await; + let resp = request().path(&format!("/users")).reply(&r).await; assert_eq!(resp.status(), 200); let body = resp.into_body(); assert!(!body.is_empty()); - let expected_response = vec![ContributionResponse { id: 1 }]; - let response: Vec = serde_json::from_slice(&body).unwrap(); + let expected_response = vec![UserResponse { + id, + username: "username".to_string(), + }]; + let response: Vec = serde_json::from_slice(&body).unwrap(); assert_eq!(response, expected_response) } #[tokio::test] #[ignore] - async fn test_delete_contribution_db() { + async fn test_delete_user_db() { let id = 1; - let new_contribution = serde_json::to_vec(&ContributionRequest { id }).unwrap(); + let new_user = serde_json::to_vec(&UserResponse { + id: 1, + username: "username".to_string(), + }) + .unwrap(); let db = init_db( "postgres://postgres:password@localhost:5432/database".to_string(), "db_test.sql".to_string(), @@ -385,15 +434,15 @@ mod tests { .unwrap(); let r = routes(db).recover(error_handler); let resp = request() - .body(new_contribution) - .path(&"/contribution") + .body(new_user) + .path(&"/users") .method("POST") .reply(&r) .await; assert_eq!(resp.status(), 200); let resp = request() - .path(&format!("/contribution/{id}")) + .path(&format!("/users/{id}")) .method("DELETE") .reply(&r) .await; @@ -401,10 +450,7 @@ mod tests { let body = resp.into_body(); assert!(body.is_empty()); - let resp = request() - .path(&format!("/contribution/{id}")) - .reply(&r) - .await; + let resp = request().path(&format!("/users/{id}")).reply(&r).await; assert_eq!(resp.status(), 404); let body = resp.into_body(); @@ -413,7 +459,7 @@ mod tests { #[tokio::test] #[ignore] - async fn test_delete_contribution_does_not_exist_db() { + async fn test_delete_user_does_not_exist_db() { let id = 1; let db = init_db( "postgres://postgres:password@localhost:5432/database".to_string(), @@ -421,11 +467,15 @@ mod tests { ) .await .unwrap(); - let new_contribution = serde_json::to_vec(&ContributionRequest { id }).unwrap(); + let new_user = serde_json::to_vec(&UserResponse { + id: 1, + username: "username".to_string(), + }) + .unwrap(); let r = routes(db).recover(error_handler); let resp = request() - .body(new_contribution) - .path(&format!("/contribution/{id}")) + .body(new_user) + .path(&format!("/users/{id}")) .method("DELETE") .reply(&r) .await; @@ -435,7 +485,7 @@ mod tests { assert!(!body.is_empty()); let expected_response = ErrorResponse { - message: "Contribution #1 not found".to_string(), + message: "User #1 not found".to_string(), }; let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); assert_eq!(response, expected_response) diff --git a/src/user/db.rs b/src/user/db.rs new file mode 100644 index 0000000..7087d6d --- /dev/null +++ b/src/user/db.rs @@ -0,0 +1,72 @@ +use mobc::async_trait; +use mobc_postgres::tokio_postgres::Row; +use warp::reject; + +use crate::db::{ + pool::DBAccess, + utils::{ + execute_query_with_timeout, query_one_timeout, query_opt_timeout, query_with_timeout, + DB_QUERY_TIMEOUT, + }, +}; + +use super::models::{User, UserRequest}; + +const TABLE: &str = "users"; + +#[async_trait] +pub trait DBUser: Send + Sync + Clone + 'static { + async fn get_user(&self, id: i32) -> Result, reject::Rejection>; + async fn get_user_by_username(&self, username: &str) + -> Result, reject::Rejection>; + async fn get_users(&self) -> Result, reject::Rejection>; + async fn create_user(&self, user: UserRequest) -> Result; + async fn delete_user(&self, id: i32) -> Result<(), reject::Rejection>; +} + +#[async_trait] +impl DBUser for DBAccess { + async fn get_user(&self, id: i32) -> Result, reject::Rejection> { + let query = format!("SELECT * FROM {} WHERE id = $1", TABLE); + match query_opt_timeout(self, query.as_str(), &[&id], DB_QUERY_TIMEOUT).await? { + Some(user) => Ok(Some(row_to_user(&user))), + None => Ok(None), + } + } + async fn get_user_by_username( + &self, + username: &str, + ) -> Result, reject::Rejection> { + let query = format!("SELECT * FROM {} WHERE username = $1", TABLE); + match query_opt_timeout(self, query.as_str(), &[&username], DB_QUERY_TIMEOUT).await? { + Some(user) => Ok(Some(row_to_user(&user))), + None => Ok(None), + } + } + + async fn get_users(&self) -> Result, reject::Rejection> { + let query = format!("SELECT * FROM {} ORDER BY created_at DESC", TABLE); + let rows = query_with_timeout(self, query.as_str(), &[], DB_QUERY_TIMEOUT).await?; + Ok(rows.iter().map(row_to_user).collect()) + } + + async fn create_user(&self, user: UserRequest) -> Result { + let query = format!("INSERT INTO {} (username) VALUES ($1) RETURNING *", TABLE); + let row = query_one_timeout(self, &query, &[&user.username], DB_QUERY_TIMEOUT).await?; + Ok(row_to_user(&row)) + } + + async fn delete_user(&self, id: i32) -> Result<(), reject::Rejection> { + let query = format!("DELETE FROM {} WHERE id = $1", TABLE); + execute_query_with_timeout(self, &query, &[&id], DB_QUERY_TIMEOUT).await + } +} + +fn row_to_user(row: &Row) -> User { + let id: i32 = row.get(0); + let username: &str = row.get(1); + User { + id, + username: username.to_string(), + } +} diff --git a/src/user/errors.rs b/src/user/errors.rs new file mode 100644 index 0000000..477d495 --- /dev/null +++ b/src/user/errors.rs @@ -0,0 +1,46 @@ +use std::fmt; + +use serde_derive::Deserialize; +use thiserror::Error; +use warp::{ + http::StatusCode, + reject::Reject, + reply::{Reply, Response}, +}; + +use crate::handlers::ErrorResponse; + +#[derive(Clone, Error, Debug, Deserialize, PartialEq)] +pub enum UserError { + UserExists(i32), + UserNotFound(i32), +} + +impl fmt::Display for UserError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + UserError::UserExists(id) => { + write!(f, "User #{} already exists", id) + } + UserError::UserNotFound(id) => { + write!(f, "User #{} not found", id) + } + } + } +} + +impl Reject for UserError {} + +impl Reply for UserError { + fn into_response(self) -> Response { + let code = match self { + UserError::UserExists(_) => StatusCode::BAD_REQUEST, + UserError::UserNotFound(_) => StatusCode::NOT_FOUND, + }; + let message = self.to_string(); + + let json = warp::reply::json(&ErrorResponse { message }); + + warp::reply::with_status(json, code).into_response() + } +} diff --git a/src/user/handlers.rs b/src/user/handlers.rs new file mode 100644 index 0000000..9ce4d1a --- /dev/null +++ b/src/user/handlers.rs @@ -0,0 +1,45 @@ +use warp::{ + http::StatusCode, + reject::Rejection, + reply::{json, Reply}, +}; + +use super::{ + db::DBUser, + errors::UserError, + models::{UserRequest, UserResponse}, +}; + +pub async fn create_user_handler( + body: UserRequest, + db_access: impl DBUser, +) -> Result { + match db_access.get_user_by_username(&body.username).await? { + Some(u) => Err(warp::reject::custom(UserError::UserExists(u.id)))?, + None => Ok(json(&UserResponse::of(db_access.create_user(body).await?))), + } +} + +pub async fn get_user_handler(id: i32, db_access: impl DBUser) -> Result { + match db_access.get_user(id).await? { + None => Err(warp::reject::custom(UserError::UserNotFound(id)))?, + Some(user) => Ok(json(&UserResponse::of(user))), + } +} + +pub async fn get_users_handler(db_access: impl DBUser) -> Result { + let users = db_access.get_users().await?; + Ok(json::>( + &users.into_iter().map(UserResponse::of).collect(), + )) +} + +pub async fn delete_user_handler(id: i32, db_access: impl DBUser) -> Result { + match db_access.get_user(id).await? { + Some(_) => { + let _ = &db_access.delete_user(id).await?; + Ok(StatusCode::OK) + } + None => Err(warp::reject::custom(UserError::UserNotFound(id)))?, + } +} diff --git a/src/user/mod.rs b/src/user/mod.rs new file mode 100644 index 0000000..93246e5 --- /dev/null +++ b/src/user/mod.rs @@ -0,0 +1,5 @@ +pub mod db; +pub mod errors; +pub mod handlers; +pub mod models; +pub mod routes; diff --git a/src/user/models.rs b/src/user/models.rs new file mode 100644 index 0000000..20e192d --- /dev/null +++ b/src/user/models.rs @@ -0,0 +1,27 @@ +use serde_derive::{Deserialize, Serialize}; + +#[derive(Deserialize)] +pub struct User { + pub id: i32, + pub username: String, +} + +#[derive(Serialize, Deserialize)] +pub struct UserRequest { + pub username: String, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct UserResponse { + pub id: i32, + pub username: String, +} + +impl UserResponse { + pub fn of(user: User) -> UserResponse { + UserResponse { + id: user.id, + username: user.username, + } + } +} diff --git a/src/user/routes.rs b/src/user/routes.rs new file mode 100644 index 0000000..a775714 --- /dev/null +++ b/src/user/routes.rs @@ -0,0 +1,44 @@ +use std::convert::Infallible; + +use warp::filters::BoxedFilter; +use warp::{Filter, Reply}; + +use super::db::DBUser; +use super::handlers; + +fn with_db( + db_pool: impl DBUser, +) -> impl Filter + Clone { + warp::any().map(move || db_pool.clone()) +} + +pub fn routes(db_access: impl DBUser) -> BoxedFilter<(impl Reply,)> { + let user = warp::path!("users"); + let user_id = warp::path!("users" / i32); + + let get_users = user + .and(warp::get()) + .and(with_db(db_access.clone())) + .and_then(handlers::get_users_handler); + + let get_user = user_id + .and(warp::get()) + // .and(warp::path::param()) + .and(with_db(db_access.clone())) + .and_then(handlers::get_user_handler); + + let create_user = user + .and(warp::post()) + .and(warp::body::json()) + .and(with_db(db_access.clone())) + .and_then(handlers::create_user_handler); + + let delete_user = user_id + .and(warp::delete()) + .and(with_db(db_access.clone())) + .and_then(handlers::delete_user_handler); + + let route = get_users.or(get_user).or(create_user).or(delete_user); + + route.boxed() +} From c2752fb12625fef91852d35b66baad0590631cda Mon Sep 17 00:00:00 2001 From: Leandro Date: Wed, 6 Mar 2024 20:43:40 -0300 Subject: [PATCH 02/98] feat: update CI --- .github/workflows/prod.yml | 71 +++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index b1f54a4..9e4a9d9 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -1,4 +1,4 @@ -name: Build, Test Prod +name: Build, Test and release to Prod on: push: @@ -10,38 +10,37 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Cache dependencies - uses: actions/cache@v3 - with: - path: | - ~/.cargo - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo- - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - toolchain: stable - - - name: Build and test code - run: | - cargo build --verbose - cargo test --verbose - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Log in to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push Docker image - run: | - docker build -t dontelmo/kudos_api:${{ github.sha }} -f Dockerfile . - docker push dontelmo/kudos_api:${{ github.sha }} \ No newline at end of file + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + + - name: Build and test code + run: | + cargo build --verbose + cargo test --verbose + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push Docker image + run: | + docker build -t ${{ vars.DOCKER_REGISTRY }}:${{ github.sha }} -f Dockerfile . + docker push ${{ vars.DOCKER_REGISTRY }}:${{ github.sha }} From b4fd83f900ad131cd0306f43d9bb04f50362cd2b Mon Sep 17 00:00:00 2001 From: Leandro Date: Wed, 6 Mar 2024 23:17:08 -0300 Subject: [PATCH 03/98] feat: use Basic auth in create/delete endpoints --- .env.example | 4 ++- Cargo.lock | 13 +++++++--- Cargo.toml | 1 + Makefile | 4 ++- src/auth.rs | 52 ++++++++++++++++++++++++++++++++++++++ src/error.rs | 44 ++++++++++++++++++++++++++++++++ src/handlers.rs | 6 ++++- src/main.rs | 2 ++ src/organization/routes.rs | 4 +++ src/repository/routes.rs | 3 +++ src/user/routes.rs | 5 +++- 11 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 src/auth.rs create mode 100644 src/error.rs diff --git a/.env.example b/.env.example index 20031a8..99545ea 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,6 @@ DATABASE_URL=postgres://postgres:password@localhost:5432/database DATABASE_INIT_FILE=db.sql HTTP_SERVER_HOST=0.0.0.0 -HTTP_SERVER_PORT=8000 \ No newline at end of file +HTTP_SERVER_PORT=8000 +USERNAME= +PASSWORD= \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 6ee9e01..af00f29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,6 +81,12 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + [[package]] name = "bitflags" version = "1.3.2" @@ -387,7 +393,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ - "base64", + "base64 0.21.5", "bytes", "headers-core", "http", @@ -540,6 +546,7 @@ dependencies = [ name = "kudos_api" version = "0.1.0" dependencies = [ + "base64 0.22.0", "chrono", "dotenv", "mobc", @@ -836,7 +843,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49b6c5ef183cd3ab4ba005f1ca64c21e8bd97ce4699cfea9e8d9a2c4958ca520" dependencies = [ - "base64", + "base64 0.21.5", "byteorder", "bytes", "fallible-iterator", @@ -935,7 +942,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64", + "base64 0.21.5", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 60302f2..e9686f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,4 @@ mobc-postgres = { version = "0.8.0", features = ["with-chrono-0_4"]} thiserror = "1.0.50" chrono = { version = "0.4.31", features= ["serde"] } serde_derive = "1.0.193" +base64 = "0.22.0" diff --git a/Makefile b/Makefile index 9606a06..4f71af1 100644 --- a/Makefile +++ b/Makefile @@ -2,10 +2,12 @@ DATABASE_URL=postgres://postgres:password@localhost:5432/database DATABASE_INIT_FILE=db.sql HTTP_SERVER_HOST=0.0.0.0 HTTP_SERVER_PORT=8000 +USERNAME= +PASSWORD= .PHONY: run run: - DATABASE_URL="$(DATABASE_URL)" DATABASE_INIT_FILE="$(DATABASE_INIT_FILE)" HTTP_SERVER_HOST="$(HTTP_SERVER_HOST)" HTTP_SERVER_PORT=$(HTTP_SERVER_PORT) cargo run + USERNAME="$(USERNAME)" PASSWORD="$(PASSWORD)" DATABASE_URL="$(DATABASE_URL)" DATABASE_INIT_FILE="$(DATABASE_INIT_FILE)" HTTP_SERVER_HOST="$(HTTP_SERVER_HOST)" HTTP_SERVER_PORT=$(HTTP_SERVER_PORT) cargo run .PHONY: test diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..ef23b0d --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,52 @@ +use crate::error::AuthenticationError; +use std::env; +use warp::{ + http::header::{HeaderMap, HeaderValue, AUTHORIZATION}, + reject, Filter, Rejection, +}; +const BASIC: &str = "Basic "; +pub fn with_auth() -> impl Filter + Clone { + warp::filters::header::headers_cloned() + .and_then(authorize) + .untuple_one() +} +async fn authorize(headers: HeaderMap) -> Result<(), Rejection> { + match token_from_header(&headers) { + Ok(token) => { + let credentials = base64::decode(token) + .map_err(|_| reject::custom(AuthenticationError::WrongCredentialsError))?; + let credentials_str = String::from_utf8(credentials) + .map_err(|_| reject::custom(AuthenticationError::WrongCredentialsError))?; + let credentials: Vec<&str> = credentials_str.split(':').collect(); + + if credentials.len() == 2 { + let expected_username = env::var("USERNAME") + .map_err(|_| reject::custom(AuthenticationError::WrongCredentialsError))?; + let expected_password = env::var("PASSWORD") + .map_err(|_| reject::custom(AuthenticationError::WrongCredentialsError))?; + + if credentials[0] == expected_username && credentials[1] == expected_password { + Ok(()) + } else { + Err(reject::custom(AuthenticationError::WrongCredentialsError)) + } + } else { + Err(reject::custom(AuthenticationError::WrongCredentialsError)) + } + } + Err(e) => Err(reject::custom(e)), + } +} + +fn token_from_header(headers: &HeaderMap) -> Result { + let header = headers + .get(AUTHORIZATION) + .ok_or(AuthenticationError::NoAuthHeaderError)?; + let auth_header = std::str::from_utf8(header.as_bytes()) + .map_err(|_| AuthenticationError::InvalidAuthHeaderError)?; + + if !auth_header.starts_with(BASIC) { + return Err(AuthenticationError::InvalidAuthHeaderError); + } + Ok(auth_header.trim_start_matches(BASIC).to_owned()) +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..e89d7cf --- /dev/null +++ b/src/error.rs @@ -0,0 +1,44 @@ +use serde::Deserialize; +use std::fmt; +use thiserror::Error; +use warp::{http::StatusCode, reject::Reject, reply::Response, Reply}; + +use crate::handlers::ErrorResponse; + +#[derive(Clone, Error, Debug, Deserialize, PartialEq)] +pub enum AuthenticationError { + WrongCredentialsError, + BasicTokenError, + NoAuthHeaderError, + InvalidAuthHeaderError, +} + +impl fmt::Display for AuthenticationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AuthenticationError::WrongCredentialsError => { + write!(f, "Wrong credentials") + } + AuthenticationError::BasicTokenError => { + write!(f, "Basic Token Error") + } + AuthenticationError::NoAuthHeaderError => write!(f, "No Authorization Header"), + AuthenticationError::InvalidAuthHeaderError => { + write!(f, "Invalid Authorization Header") + } + } + } +} + +impl Reject for AuthenticationError {} + +impl Reply for AuthenticationError { + fn into_response(self) -> Response { + let code = StatusCode::UNAUTHORIZED; + let message = self.to_string(); + + let json = warp::reply::json(&ErrorResponse { message }); + + warp::reply::with_status(json, code).into_response() + } +} diff --git a/src/handlers.rs b/src/handlers.rs index 652659c..55839ab 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -4,7 +4,8 @@ use std::convert::Infallible; use warp::{hyper::StatusCode, Rejection, Reply}; use crate::{ - db::errors::DBError, organization::errors::OrganizationError, user::errors::UserError, + db::errors::DBError, error::AuthenticationError, organization::errors::OrganizationError, + user::errors::UserError, }; #[derive(Serialize, Deserialize, PartialEq, Debug)] @@ -13,10 +14,13 @@ pub struct ErrorResponse { } pub async fn error_handler(err: Rejection) -> std::result::Result { + // TODO: improve this if let Some(e) = err.find::() { Ok(e.clone().into_response()) } else if let Some(e) = err.find::() { Ok(e.clone().into_response()) + } else if let Some(e) = err.find::() { + Ok(e.clone().into_response()) } else if let Some(e) = err.find::() { let (code, message) = match e { DBError::DBPoolConnection(_) => ( diff --git a/src/main.rs b/src/main.rs index bcfd515..ca38db3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,9 @@ use warp::Filter; use crate::types::ApiConfig; +mod auth; mod db; +mod error; mod handlers; mod health; mod organization; diff --git a/src/organization/routes.rs b/src/organization/routes.rs index 4d8d04b..cf1883d 100644 --- a/src/organization/routes.rs +++ b/src/organization/routes.rs @@ -3,6 +3,8 @@ use std::convert::Infallible; use warp::filters::BoxedFilter; use warp::{Filter, Reply}; +use crate::auth::with_auth; + use super::db::DBOrganization; use super::handlers; @@ -28,12 +30,14 @@ pub fn routes(db_access: impl DBOrganization) -> BoxedFilter<(impl Reply,)> { .and_then(handlers::get_organization_handler); let create_organization = organization + .and(with_auth()) .and(warp::post()) .and(warp::body::json()) .and(with_db(db_access.clone())) .and_then(handlers::create_organization_handler); let delete_organization = organization_id + .and(with_auth()) .and(warp::delete()) .and(with_db(db_access.clone())) .and_then(handlers::delete_organization_handler); diff --git a/src/repository/routes.rs b/src/repository/routes.rs index 5b2a99e..08cbdb8 100644 --- a/src/repository/routes.rs +++ b/src/repository/routes.rs @@ -3,6 +3,7 @@ use std::convert::Infallible; use warp::filters::BoxedFilter; use warp::{Filter, Reply}; +use crate::auth::with_auth; use crate::organization::db::DBOrganization; use super::db::DBRepository; @@ -30,12 +31,14 @@ pub fn routes(db_access: impl DBRepository + DBOrganization) -> BoxedFilter<(imp .and_then(handlers::get_repository_handler); let create_repository = repository + .and(with_auth()) .and(warp::post()) .and(warp::body::json()) .and(with_db(db_access.clone())) .and_then(handlers::create_repository_handler); let delete_repository = repository_id + .and(with_auth()) .and(warp::delete()) .and(with_db(db_access.clone())) .and_then(handlers::delete_repository_handler); diff --git a/src/user/routes.rs b/src/user/routes.rs index a775714..c6803e3 100644 --- a/src/user/routes.rs +++ b/src/user/routes.rs @@ -3,6 +3,8 @@ use std::convert::Infallible; use warp::filters::BoxedFilter; use warp::{Filter, Reply}; +use crate::auth::with_auth; + use super::db::DBUser; use super::handlers; @@ -23,17 +25,18 @@ pub fn routes(db_access: impl DBUser) -> BoxedFilter<(impl Reply,)> { let get_user = user_id .and(warp::get()) - // .and(warp::path::param()) .and(with_db(db_access.clone())) .and_then(handlers::get_user_handler); let create_user = user + .and(with_auth()) .and(warp::post()) .and(warp::body::json()) .and(with_db(db_access.clone())) .and_then(handlers::create_user_handler); let delete_user = user_id + .and(with_auth()) .and(warp::delete()) .and(with_db(db_access.clone())) .and_then(handlers::delete_user_handler); From 5f3353bc77eeeba90b8d5c61a6ce5cf21678a9d1 Mon Sep 17 00:00:00 2001 From: Leandro Date: Thu, 7 Mar 2024 22:32:48 -0300 Subject: [PATCH 04/98] feat: add issue endpoints (WIP) --- Cargo.lock | 1 + Cargo.toml | 1 + README.md | 19 +++++------ db.sql | 18 ++++++++-- src/issue/db.rs | 77 +++++++++++++++++++++++++++++++++++++++++++ src/issue/errors.rs | 51 ++++++++++++++++++++++++++++ src/issue/handlers.rs | 60 +++++++++++++++++++++++++++++++++ src/issue/mod.rs | 6 ++++ src/issue/models.rs | 44 +++++++++++++++++++++++++ src/issue/routes.rs | 52 +++++++++++++++++++++++++++++ src/issue/utils.rs | 27 +++++++++++++++ src/main.rs | 2 ++ 12 files changed, 345 insertions(+), 13 deletions(-) create mode 100644 src/issue/db.rs create mode 100644 src/issue/errors.rs create mode 100644 src/issue/handlers.rs create mode 100644 src/issue/mod.rs create mode 100644 src/issue/models.rs create mode 100644 src/issue/routes.rs create mode 100644 src/issue/utils.rs diff --git a/Cargo.lock b/Cargo.lock index af00f29..c19d799 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -556,6 +556,7 @@ dependencies = [ "serde_json", "thiserror", "tokio", + "url", "warp", ] diff --git a/Cargo.toml b/Cargo.toml index e9686f1..67ca78b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,4 @@ thiserror = "1.0.50" chrono = { version = "0.4.31", features= ["serde"] } serde_derive = "1.0.193" base64 = "0.22.0" +url = "2.5.0" diff --git a/README.md b/README.md index e833b29..635eac4 100644 --- a/README.md +++ b/README.md @@ -10,17 +10,17 @@ This Dockerfile requires BuildKit and buildx. BuildKit is an improved backend to Enable it by setting: -```export DOCKER_BUILDKIT=1 ``` +`export DOCKER_BUILDKIT=1 ` ### Build To build the image, use: -```docker build . -t kudos-api``` +`docker build . -t kudos-api` ### Run -```docker run -e DATABASE_URL=... -e HTTP_SERVER_HOST=... -e HTTP_SERVER_PORT=... kudos-api``` +`docker run -e DATABASE_URL=... -e HTTP_SERVER_HOST=... -e HTTP_SERVER_PORT=... kudos-api` ### Docker-compose @@ -28,11 +28,11 @@ To build the image, use: It builds the image if it's the first time, otherwise, it uses the latest built image. -```docker-compose up``` +`docker-compose up` ### Build and run -```docker-compose up --build``` +`docker-compose up --build` ## Test @@ -40,14 +40,13 @@ It builds the image if it's the first time, otherwise, it uses the latest built Run the command: -```make test``` +`make test` ### DB tests -These tests needs a real postgres DB running. You can start a new one using ```docker-compose up db``` and then running the tests: +These tests needs a real postgres DB running. You can start a new one using `docker-compose up db` and then running the tests: - -```make test-db``` +`make test-db` Note: the tests will delete some tables before running. Use a dummy DB! @@ -55,4 +54,4 @@ Note: the tests will delete some tables before running. Use a dummy DB! ## Workflow -This repository is connected to render.com and will trigger a new deployment to production when a new commit arrives in main branch. \ No newline at end of file +This repository is connected to render.com and will trigger a new deployment to production when a new commit arrives in main branch. diff --git a/db.sql b/db.sql index b7e3289..496f50e 100644 --- a/db.sql +++ b/db.sql @@ -12,13 +12,25 @@ CREATE TABLE IF NOT EXISTS repositories ( id SERIAL PRIMARY KEY, name VARCHAR(255), organization_id INT REFERENCES organizations(id), - -- user_id INT REFERENCES users(id), // TODO: allow users created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') ); CREATE TABLE IF NOT EXISTS issues ( id SERIAL PRIMARY KEY, issue_number INT, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), - updated_at TIMESTAMP, - repository_id INT REFERENCES repositories(id) -- TODO: add "tipping" status + repository_id INT REFERENCES repositories(id), + tip_id INT REFERENCES tips(id) +); +CREATE TABLE IF NOT EXISTS tips ( + id SERIAL PRIMARY KEY, + issue_id INT REFERENCES issues(id), + url VARCHAR(255) UNIQUE, + -- TODO: extra fields of tipping + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') +); +-- Adding a many-to-many relationship table for users and repositories +CREATE TABLE IF NOT EXISTS repository_users ( + repository_id INT REFERENCES repositories(id), + user_id INT REFERENCES users(id), + PRIMARY KEY (repository_id, user_id) ); \ No newline at end of file diff --git a/src/issue/db.rs b/src/issue/db.rs new file mode 100644 index 0000000..b178fc7 --- /dev/null +++ b/src/issue/db.rs @@ -0,0 +1,77 @@ +use mobc::async_trait; +use mobc_postgres::tokio_postgres::Row; +use warp::reject; + +use crate::db::{ + pool::DBAccess, + utils::{ + execute_query_with_timeout, query_one_timeout, query_opt_timeout, query_with_timeout, + DB_QUERY_TIMEOUT, + }, +}; + +use super::models::{Issue, IssueCreate}; +use super::utils::parse_github_issue_url; + +const TABLE: &str = "issues"; + +#[async_trait] +pub trait DBIssue: Send + Sync + Clone + 'static { + async fn get_issue(&self, id: i32) -> Result, reject::Rejection>; + async fn get_issue_by_url(&self, url: &str) -> Result, reject::Rejection>; + async fn get_issues(&self) -> Result, reject::Rejection>; + async fn create_issue(&self, issue: IssueCreate) -> Result; + async fn delete_issue(&self, id: i32) -> Result<(), reject::Rejection>; +} + +#[async_trait] +impl DBIssue for DBAccess { + async fn get_issue(&self, id: i32) -> Result, reject::Rejection> { + let query = format!("SELECT * FROM {} WHERE id = $1", TABLE); + match query_opt_timeout(self, query.as_str(), &[&id], DB_QUERY_TIMEOUT).await? { + Some(user) => Ok(Some(row_to_issue(&user))), + None => Ok(None), + } + } + + async fn get_issue_by_url(&self, url: &str) -> Result, reject::Rejection> { + let query = format!("SELECT * FROM {} WHERE url = $1", TABLE); + match query_opt_timeout(self, query.as_str(), &[&url], DB_QUERY_TIMEOUT).await? { + Some(user) => Ok(Some(row_to_issue(&user))), + None => Ok(None), + } + } + + async fn get_issues(&self) -> Result, reject::Rejection> { + let query = format!("SELECT * FROM {} ORDER BY created_at DESC", TABLE); + let rows = query_with_timeout(self, query.as_str(), &[], DB_QUERY_TIMEOUT).await?; + Ok(rows.iter().map(row_to_issue).collect()) + } + + async fn create_issue(&self, issue: IssueCreate) -> Result { + let query = format!( + "INSERT INTO {} (issue_number, repository_id, url) VALUES ($1, $2, $3) RETURNING *", + TABLE + ); + let row = query_one_timeout( + self, + &query, + &[&issue.id, &issue.repository_id, &issue.url], + DB_QUERY_TIMEOUT, + ) + .await?; + Ok(row_to_issue(&row)) + } + + async fn delete_issue(&self, id: i32) -> Result<(), reject::Rejection> { + let query = format!("DELETE FROM {} WHERE id = $1", TABLE); + execute_query_with_timeout(self, &query, &[&id], DB_QUERY_TIMEOUT).await + } +} + +fn row_to_issue(row: &Row) -> Issue { + let id: i32 = row.get(0); + let repository_id: i32 = row.get(1); + // let url: &str = row.get(2); //TODO: define if we need it in the response + Issue { id, repository_id } +} diff --git a/src/issue/errors.rs b/src/issue/errors.rs new file mode 100644 index 0000000..f36dedc --- /dev/null +++ b/src/issue/errors.rs @@ -0,0 +1,51 @@ +use std::fmt; + +use serde_derive::Deserialize; +use thiserror::Error; +use warp::{ + http::StatusCode, + reject::Reject, + reply::{Reply, Response}, +}; + +use crate::handlers::ErrorResponse; + +#[derive(Clone, Error, Debug, Deserialize, PartialEq)] +pub enum IssueError { + IssueExists(i32), + IssueNotFound(i32), + IssueInvalidURL, +} + +impl fmt::Display for IssueError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + IssueError::IssueExists(id) => { + write!(f, "Issue #{} already exists", id) + } + IssueError::IssueNotFound(id) => { + write!(f, "Issue #{} not found", id) + } + IssueError::IssueInvalidURL => { + write!(f, "Issue url is invalid") + } + } + } +} + +impl Reject for IssueError {} + +impl Reply for IssueError { + fn into_response(self) -> Response { + let code = match self { + IssueError::IssueExists(_) => StatusCode::BAD_REQUEST, + IssueError::IssueNotFound(_) => StatusCode::NOT_FOUND, + IssueError::IssueInvalidURL => StatusCode::BAD_REQUEST, + }; + let message = self.to_string(); + + let json = warp::reply::json(&ErrorResponse { message }); + + warp::reply::with_status(json, code).into_response() + } +} diff --git a/src/issue/handlers.rs b/src/issue/handlers.rs new file mode 100644 index 0000000..dd066a5 --- /dev/null +++ b/src/issue/handlers.rs @@ -0,0 +1,60 @@ +use warp::{ + http::StatusCode, + reject::Rejection, + reply::{json, Reply}, +}; + +use crate::{organization::db::DBOrganization, repository::db::DBRepository}; + +use super::{ + db::DBIssue, + errors::IssueError, + models::{IssueCreateRequest, IssueGetRequest, IssueResponse}, + utils::parse_github_issue_url, +}; + +pub async fn create_issue_handler( + body: IssueCreateRequest, + db_access: impl DBIssue + DBOrganization + DBRepository, +) -> Result { + match db_access.get_issue_by_url(&body.url).await? { + Some(u) => Err(warp::reject::custom(IssueError::IssueExists(u.id)))?, + None => { + let info = parse_github_issue_url(&body.url)?; + // TODO: get or create both + // db_access.create_organization(info.organization); + // db_access.create_repository(info.repository); + // Ok(json(&IssueResponse::of( + // db_access.create_issue(body).await?, + // ))) + Ok(StatusCode::OK) //TODO: added to avoid errors in IDE + } + } +} + +pub async fn get_issue_handler(id: i32, db_access: impl DBIssue) -> Result { + match db_access.get_issue(id).await? { + None => Err(warp::reject::custom(IssueError::IssueNotFound(id)))?, + Some(issue) => Ok(json(&IssueResponse::of(issue))), + } +} + +pub async fn get_issues_handler(db_access: impl DBIssue) -> Result { + let issues = db_access.get_issues().await?; + Ok(json::>( + &issues.into_iter().map(IssueResponse::of).collect(), + )) +} + +pub async fn delete_issue_handler( + id: i32, + db_access: impl DBIssue, +) -> Result { + match db_access.get_issue(id).await? { + Some(_) => { + let _ = &db_access.delete_issue(id).await?; + Ok(StatusCode::OK) + } + None => Err(warp::reject::custom(IssueError::IssueNotFound(id)))?, + } +} diff --git a/src/issue/mod.rs b/src/issue/mod.rs new file mode 100644 index 0000000..b6041a3 --- /dev/null +++ b/src/issue/mod.rs @@ -0,0 +1,6 @@ +pub mod db; +pub mod errors; +pub mod handlers; +pub mod models; +pub mod routes; +pub mod utils; diff --git a/src/issue/models.rs b/src/issue/models.rs new file mode 100644 index 0000000..b93a455 --- /dev/null +++ b/src/issue/models.rs @@ -0,0 +1,44 @@ +use serde_derive::{Deserialize, Serialize}; + +#[derive(Deserialize)] +pub struct Issue { + pub id: i32, + pub repository_id: i32, +} + +#[derive(Serialize, Deserialize)] +pub struct IssueCreateRequest { + pub url: String, +} +#[derive(Serialize, Deserialize)] +pub struct IssueGetRequest { + pub id: i32, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct IssueResponse { + pub id: i32, + pub repository_id: i32, + // TODO: add tip +} + +impl IssueResponse { + pub fn of(issue: Issue) -> IssueResponse { + IssueResponse { + id: issue.id, + repository_id: issue.repository_id, + } + } +} + +pub struct IssueInfo { + pub organization: String, + pub repository: String, + pub issue_id: u32, +} + +pub struct IssueCreate { + pub repository_id: u32, + pub id: u32, + pub url: String, +} diff --git a/src/issue/routes.rs b/src/issue/routes.rs new file mode 100644 index 0000000..14d9913 --- /dev/null +++ b/src/issue/routes.rs @@ -0,0 +1,52 @@ +use std::convert::Infallible; + +use warp::filters::BoxedFilter; +use warp::{Filter, Reply}; + +use crate::auth::with_auth; +use crate::organization::db::DBOrganization; +use crate::repository::db::DBRepository; + +use super::db::DBIssue; +use super::handlers; + +fn with_db( + db_pool: impl DBIssue + DBOrganization + DBRepository, +) -> impl Filter + Clone +{ + warp::any().map(move || db_pool.clone()) +} + +pub fn routes( + db_access: impl DBIssue + DBOrganization + DBRepository, +) -> BoxedFilter<(impl Reply,)> { + let issue = warp::path!("issues"); + let issue_id = warp::path!("issues" / i32); + + let get_issues = issue + .and(warp::get()) + .and(with_db(db_access.clone())) + .and_then(handlers::get_issues_handler); + + let get_issue = issue_id + .and(warp::get()) + .and(with_db(db_access.clone())) + .and_then(handlers::get_issue_handler); + + let create_issue = issue + .and(with_auth()) + .and(warp::post()) + .and(warp::body::json()) + .and(with_db(db_access.clone())) + .and_then(handlers::create_issue_handler); + + let delete_issue = issue_id + .and(with_auth()) + .and(warp::delete()) + .and(with_db(db_access.clone())) + .and_then(handlers::delete_issue_handler); + + let route = get_issues.or(get_issue).or(create_issue).or(delete_issue); + + route.boxed() +} diff --git a/src/issue/utils.rs b/src/issue/utils.rs new file mode 100644 index 0000000..fc00ab6 --- /dev/null +++ b/src/issue/utils.rs @@ -0,0 +1,27 @@ +use super::{errors::IssueError, models::IssueInfo}; +use url::Url; + +pub fn parse_github_issue_url(url_str: &str) -> Result { + let url = Url::parse(url_str).map_err(|_| IssueError::IssueInvalidURL)?; + let path_segments: Vec<&str> = url + .path_segments() + .ok_or(IssueError::IssueInvalidURL)? + .collect(); + if path_segments.len() >= 4 && path_segments[0] == "github.com" && path_segments[2] == "issues" + { + // Extract organization, repository, and issue id + let organization = path_segments[1].to_owned(); + let repository = path_segments[2].to_owned(); + let issue_id = path_segments[3] + .parse::() + .map_err(|_| IssueError::IssueInvalidURL)?; + + Ok(IssueInfo { + organization, + repository, + issue_id, + }) + } else { + Err(IssueError::IssueInvalidURL) + } +} diff --git a/src/main.rs b/src/main.rs index ca38db3..ea48875 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ mod db; mod error; mod handlers; mod health; +mod issue; mod organization; mod repository; mod user; @@ -37,6 +38,7 @@ async fn run() { let users_route = user::routes::routes(db.clone()); let organizations_route = organization::routes::routes(db.clone()); let repositories_route = repository::routes::routes(db); + //TODO: add issue route let error_handler = handlers::error_handler; // string all the routes together From 29b397c9d6295c2313862ec5b6b51744167b1422 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sat, 13 Apr 2024 16:45:52 -0300 Subject: [PATCH 05/98] feat: decouple db init and db conn. Improve db scheme --- Makefile | 28 +++++++- db.sql | 159 ++++++++++++++++++++++++++++++++++++++++---- docker-compose.yaml | 9 ++- src/main.rs | 24 ++++++- src/types.rs | 3 +- 5 files changed, 199 insertions(+), 24 deletions(-) diff --git a/Makefile b/Makefile index 4f71af1..5291e9d 100644 --- a/Makefile +++ b/Makefile @@ -4,17 +4,41 @@ HTTP_SERVER_HOST=0.0.0.0 HTTP_SERVER_PORT=8000 USERNAME= PASSWORD= +DOCKER_DB_CONTAINER_NAME:=db +DOCKER_COMPOSE:=docker-compose +DOCKER_COMPOSE_FILE:=docker-compose.yaml + +# API .PHONY: run run: - USERNAME="$(USERNAME)" PASSWORD="$(PASSWORD)" DATABASE_URL="$(DATABASE_URL)" DATABASE_INIT_FILE="$(DATABASE_INIT_FILE)" HTTP_SERVER_HOST="$(HTTP_SERVER_HOST)" HTTP_SERVER_PORT=$(HTTP_SERVER_PORT) cargo run - + USERNAME="$(USERNAME)" PASSWORD="$(PASSWORD)" DATABASE_URL="$(DATABASE_URL)" HTTP_SERVER_HOST="$(HTTP_SERVER_HOST)" HTTP_SERVER_PORT=$(HTTP_SERVER_PORT) cargo run .PHONY: test test: cargo test +# DB + +# Start the PostgreSQL container +.PHONY: db-up +db-up: + $(DOCKER_COMPOSE) -f $(DOCKER_COMPOSE_FILE) up $(DOCKER_DB_CONTAINER_NAME) -d + +# Stop and remove the PostgreSQL container +.PHONY: db-down +db-down: + $(DOCKER_COMPOSE) -f $(DOCKER_COMPOSE_FILE) down $(DOCKER_DB_CONTAINER_NAME) + +.PHONY: db-init +db-init: + USERNAME="$(USERNAME)" PASSWORD="$(PASSWORD)" DATABASE_URL="$(DATABASE_URL)" DATABASE_INIT_FILE="$(DATABASE_INIT_FILE)" cargo run +# Clean up the Docker volume +.PHONY: db-clean +db-clean: + $(DOCKER_COMPOSE) -f $(DOCKER_COMPOSE_FILE) down $(DOCKER_DB_CONTAINER_NAME) -v + .PHONY: test-db test-db: cargo test -- --ignored --test-threads=1 diff --git a/db.sql b/db.sql index 496f50e..dd811e2 100644 --- a/db.sql +++ b/db.sql @@ -1,36 +1,169 @@ +-- some enums +CREATE TYPE tip_status AS ENUM ('set', 'paid', 'rejected'); +CREATE TYPE issue_status AS ENUM ('open', 'closed'); +CREATE TYPE tip_type AS ENUM ('direct', 'gov'); +-- all the users including maintainers and admins CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username VARCHAR(100) UNIQUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') ); +-- basic github organization CREATE TABLE IF NOT EXISTS organizations ( id SERIAL PRIMARY KEY, name VARCHAR(255) UNIQUE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') + icon VARCHAR(255), + description VARCHAR(255), + url VARCHAR(255), + email VARCHAR(255), + twitter VARCHAR(255), + e_tag VARCHAR(40) NOT NULL, + is_verified boolean, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), + updated_at TIMESTAMP NULL ); +-- basic github repository CREATE TABLE IF NOT EXISTS repositories ( id SERIAL PRIMARY KEY, name VARCHAR(255), + url VARCHAR(255), + icon VARCHAR(255), + e_tag VARCHAR(40) NOT NULL, organization_id INT REFERENCES organizations(id), - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), + updated_at TIMESTAMP NULL ); -CREATE TABLE IF NOT EXISTS issues ( +-- used in the issues +CREATE TABLE IF NOT EXISTS labels ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) UNIQUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), + updated_at TIMESTAMP NULL +); +-- used in the repositories +CREATE TABLE IF NOT EXISTS topics ( id SERIAL PRIMARY KEY, - issue_number INT, + name VARCHAR(255) UNIQUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), - repository_id INT REFERENCES repositories(id), - tip_id INT REFERENCES tips(id) + updated_at TIMESTAMP NULL +); +CREATE TABLE IF NOT EXISTS languages ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) UNIQUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), + updated_at TIMESTAMP NULL +); +-- one repository can have multiple languages +-- one language can have multiple repositories +CREATE TABLE IF NOT EXISTS repositories_languages ( + id SERIAL PRIMARY KEY, + repositories_id INT REFERENCES repositories(id), + languages_id INT REFERENCES languages(id) +); +-- one repository can have multiple topics +-- one topic can have multiple repositories +CREATE TABLE IF NOT EXISTS repositories_topics ( + id SERIAL PRIMARY KEY, + repositories_id INT REFERENCES repositories(id), + topics_id INT REFERENCES topics(id) +); +-- used in the tips +CREATE TABLE IF NOT EXISTS blockchain ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) UNIQUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), + updated_at TIMESTAMP NULL ); +-- filters used by the frontend: languages, interests, etc +CREATE TABLE IF NOT EXISTS filters ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) UNIQUE, + emoji TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), + updated_at TIMESTAMP NULL +); +-- filters used by the frontend: EVM, documentation, etc. +CREATE TABLE IF NOT EXISTS filter_values ( + id SERIAL PRIMARY KEY, + filters_id INT REFERENCES filters(id) NOT NULL, + emoji TEXT NOT NULL, + name VARCHAR(255) UNIQUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), + updated_at TIMESTAMP NULL +); +-- we need to associate the repository (and then the issue) to some +-- filters that are used by the frontend. +-- For example, "Interests" -> "EVM" and "Network" -> "Kusama", "Polkadot" +-- one repository can have multiple filters +-- one topic can have multiple repositories +CREATE TABLE IF NOT EXISTS repositories_filters ( + id SERIAL PRIMARY KEY, + repositories_id INT REFERENCES repositories(id), + filters_id INT REFERENCES filters(id), + filter_values_id INT REFERENCES filter_values(id) +); +-- two types of tips: gov and direct. One tip per issue CREATE TABLE IF NOT EXISTS tips ( + id SERIAL PRIMARY KEY, + status tip_status NOT NULL, + type tip_type NOT NULL, + amount BIGINT CHECK (amount >= 0) NOT NULL, + "to" VARCHAR(48) NOT NULL, + "from" VARCHAR(48) NOT NULL, + --direct + transaction VARCHAR(255) NULL, + blockchain_id INT REFERENCES blockchain(id) NULL, + -- gov + url VARCHAR(255) NULL, + contributor_id INT REFERENCES users(id) NOT NULL, + --not needed if it's a proposal + curator_id INT REFERENCES users(id) NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), + updated_at TIMESTAMP NULL +); +-- issue with repository, user and tip +CREATE TABLE IF NOT EXISTS issues ( + id SERIAL PRIMARY KEY, + issue_number INT NOT NULL, + url VARCHAR(255), + title VARCHAR(255), + description VARCHAR(255), + status issue_status NOT NULL, + repository_id INT REFERENCES repositories(id) NOT NULL, + user_id INT REFERENCES users(id) NULL, + tip_id INT REFERENCES tips(id) NULL, + issue_created_at TIMESTAMP NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), + e_tag VARCHAR(40) NOT NULL, + updated_at TIMESTAMP NULL +); +-- one issue can have multiple labels +-- one label can have multiple issues +CREATE TABLE IF NOT EXISTS issues_labels ( id SERIAL PRIMARY KEY, issue_id INT REFERENCES issues(id), - url VARCHAR(255) UNIQUE, - -- TODO: extra fields of tipping - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') + labels_id INT REFERENCES labels(id) ); --- Adding a many-to-many relationship table for users and repositories -CREATE TABLE IF NOT EXISTS repository_users ( +-- one user can be maintainer in multiple repositories +-- one repository can have multiple maintainers +CREATE TABLE IF NOT EXISTS maintainers ( + id SERIAL PRIMARY KEY, repository_id INT REFERENCES repositories(id), - user_id INT REFERENCES users(id), - PRIMARY KEY (repository_id, user_id) + user_id INT REFERENCES users(id) +); +-- wishes are an special issue in some well known repositories +CREATE TABLE IF NOT EXISTS wishes ( + id SERIAL PRIMARY KEY, + issue_number INT NOT NULL, + url VARCHAR(255), + title VARCHAR(255), + description VARCHAR(255), + status issue_status NOT NULL, + repository_id INT REFERENCES repositories(id) NOT NULL, + user_id INT REFERENCES users(id) NULL, + tip_id INT REFERENCES tips(id) NULL, + issue_created_at TIMESTAMP NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), + e_tag VARCHAR(40) NOT NULL, + updated_at TIMESTAMP NULL ); \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 5533797..b825e15 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -11,7 +11,6 @@ services: - "8000:8000" environment: - DATABASE_URL=postgres://postgres:password@db:5432/database - - DATABASE_INIT_FILE=db.sql # TODO: remove - HTTP_SERVER_HOST=0.0.0.0 - HTTP_SERVER_PORT=8000 depends_on: @@ -27,11 +26,11 @@ services: POSTGRES_DB: database POSTGRES_USER: postgres POSTGRES_PASSWORD: password -# volumes: -# - pgdata:/var/lib/postgresql/data + volumes: + - pgdata:/var/lib/postgresql/data -# volumes: -# pgdata: +volumes: + pgdata: networks: kudos: diff --git a/src/main.rs b/src/main.rs index ea48875..fe2aefc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,17 @@ mod types; +use std::process; + use db::utils::init_db; use warp::Filter; -use crate::types::ApiConfig; +use crate::{ + db::{ + errors::DBError, + pool::{DBAccess, DBAccessor}, + }, + types::ApiConfig, +}; mod auth; mod db; @@ -32,7 +40,19 @@ async fn run() { } = ApiConfig::new(); // init db - let db = init_db(database_url, database_init_file).await.unwrap(); //If there's an error the api should panic + let db_pool = db::pool::create_pool(&database_url) + .map_err(DBError::DBPoolConnection) + .expect("Cannot create DB connection"); + let db = DBAccess::new(db_pool); + + // create db and exit + if database_init_file != "" { + db.init_db(&database_init_file) + .await + .expect("Cannot create DB scheme"); + print!("DB schem created"); + process::exit(0); + } let health_route = health::routes::routes(db.clone()); let users_route = user::routes::routes(db.clone()); diff --git a/src/types.rs b/src/types.rs index 6601f27..c1a4017 100644 --- a/src/types.rs +++ b/src/types.rs @@ -25,8 +25,7 @@ impl ApiConfig { .parse() .expect("Invalid HTTP_SERVER_PORT"), database_url: env::var("DATABASE_URL").expect("DATABASE_URL must be set"), - database_init_file: env::var("DATABASE_INIT_FILE") - .expect("DATABASE_INIT_FILE must be set"), + database_init_file: env::var("DATABASE_INIT_FILE").unwrap_or_else(|_| "".to_owned()), } } } From 9a922a6cc7e3a6a6812b842d54825dd62ca3cf47 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sat, 13 Apr 2024 23:00:23 -0300 Subject: [PATCH 06/98] feat: add migrations, add query params and db join/where --- .env.example | 5 +- .gitignore | 3 +- Cargo.lock | 170 ++- Cargo.toml | 7 +- Makefile | 10 +- db_test.sql | 6 - diesel.toml | 9 + migrations/.keep | 0 .../down.sql | 5 + .../up.sql | 36 + migrations/2024-04-13-204151_init/down.sql | 23 + .../2024-04-13-204151_init/up.sql | 159 +-- src/main.rs | 9 - src/tests/contribution.rs | 986 +++++++++--------- src/tests/health.rs | 35 +- src/user/db.rs | 39 +- src/user/handlers.rs | 21 +- src/user/models.rs | 50 +- src/user/routes.rs | 5 +- 19 files changed, 897 insertions(+), 681 deletions(-) delete mode 100644 db_test.sql create mode 100644 diesel.toml create mode 100644 migrations/.keep create mode 100644 migrations/00000000000000_diesel_initial_setup/down.sql create mode 100644 migrations/00000000000000_diesel_initial_setup/up.sql create mode 100644 migrations/2024-04-13-204151_init/down.sql rename db.sql => migrations/2024-04-13-204151_init/up.sql (83%) diff --git a/.env.example b/.env.example index 99545ea..c9b8fe4 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,5 @@ DATABASE_URL=postgres://postgres:password@localhost:5432/database -DATABASE_INIT_FILE=db.sql HTTP_SERVER_HOST=0.0.0.0 HTTP_SERVER_PORT=8000 -USERNAME= -PASSWORD= \ No newline at end of file +USERNAME=test +PASSWORD=test \ No newline at end of file diff --git a/.gitignore b/.gitignore index ccb5166..b5e703e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target -.vscode \ No newline at end of file +.vscode +.env \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index c19d799..9f1a1bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,13 +19,14 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.7.7" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ - "getrandom", + "cfg-if", "once_cell", "version_check", + "zerocopy", ] [[package]] @@ -51,7 +52,7 @@ checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn", ] [[package]] @@ -93,6 +94,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + [[package]] name = "block-buffer" version = "0.10.4" @@ -181,6 +188,40 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +[[package]] +name = "diesel" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03fc05c17098f21b89bc7d98fe1dd3cce2c11c2ad8e145f2a44fe08ed28eb559" +dependencies = [ + "bitflags 2.5.0", + "byteorder", + "diesel_derives", + "itoa", + "pq-sys", +] + +[[package]] +name = "diesel_derives" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d02eecb814ae714ffe61ddc2db2dd03e6c49a42e269b5001355500d431cce0c" +dependencies = [ + "diesel_table_macro_syntax", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" +dependencies = [ + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -284,9 +325,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" @@ -296,7 +337,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn", ] [[package]] @@ -313,9 +354,9 @@ checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-timer" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" @@ -413,9 +454,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hmac" @@ -548,6 +589,7 @@ version = "0.1.0" dependencies = [ "base64 0.22.0", "chrono", + "diesel", "dotenv", "mobc", "mobc-postgres", @@ -606,23 +648,12 @@ checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "metrics" -version = "0.18.1" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e52eb6380b6d2a10eb3434aec0885374490f5b82c8aaf5cd487a183c98be834" +checksum = "2be3cbd384d4e955b231c895ce10685e3d8260c5ccffae898c96c723b0772835" dependencies = [ "ahash", - "metrics-macros", -] - -[[package]] -name = "metrics-macros" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49e30813093f757be5cf21e50389a24dc7dbb22c49f23b7e8f51d69b508a5ffa" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "portable-atomic", ] [[package]] @@ -663,9 +694,9 @@ dependencies = [ [[package]] name = "mobc" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90eb49dc5d193287ff80e72a86f34cfb27aae562299d22fea215e06ea1059dd3" +checksum = "d8d3681f0b299413df040f53c6950de82e48a8e1a9f79d442ed1ad3694d660b9" dependencies = [ "async-trait", "futures-channel", @@ -823,7 +854,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn", ] [[package]] @@ -838,6 +869,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + [[package]] name = "postgres-protocol" version = "0.6.6" @@ -874,6 +911,15 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pq-sys" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0052426df997c0cbd30789eb44ca097e3541717a7b8fa36b1c464ee7edebd" +dependencies = [ + "vcpkg", +] + [[package]] name = "proc-macro2" version = "1.0.70" @@ -928,7 +974,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -981,7 +1027,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn", ] [[package]] @@ -1102,17 +1148,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.39" @@ -1141,14 +1176,14 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn", ] [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", @@ -1194,7 +1229,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn", ] [[package]] @@ -1286,7 +1321,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn", ] [[package]] @@ -1408,6 +1443,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -1460,6 +1501,12 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.89" @@ -1481,7 +1528,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn", "wasm-bindgen-shared", ] @@ -1503,7 +1550,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1526,11 +1573,12 @@ dependencies = [ [[package]] name = "whoami" -version = "1.4.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" dependencies = [ - "wasm-bindgen", + "redox_syscall", + "wasite", "web-sys", ] @@ -1630,3 +1678,23 @@ name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 67ca78b..9ff818d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,13 +8,14 @@ edition = "2021" [dependencies] warp = "0.3" tokio = { version = "1.34", features = ["macros"] } -serde = { version = "1.0" , features = ["derive"]} +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" dotenv = "0.15" mobc = "0.8.3" -mobc-postgres = { version = "0.8.0", features = ["with-chrono-0_4"]} +mobc-postgres = { version = "0.8.0", features = ["with-chrono-0_4"] } thiserror = "1.0.50" -chrono = { version = "0.4.31", features= ["serde"] } +chrono = { version = "0.4.31", features = ["serde"] } serde_derive = "1.0.193" base64 = "0.22.0" url = "2.5.0" +diesel = { version = "2.1.5", features = ["postgres"] } diff --git a/Makefile b/Makefile index 5291e9d..79ac86b 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,8 @@ DATABASE_URL=postgres://postgres:password@localhost:5432/database DATABASE_INIT_FILE=db.sql HTTP_SERVER_HOST=0.0.0.0 HTTP_SERVER_PORT=8000 -USERNAME= -PASSWORD= +USERNAME=test +PASSWORD=test DOCKER_DB_CONTAINER_NAME:=db DOCKER_COMPOSE:=docker-compose DOCKER_COMPOSE_FILE:=docker-compose.yaml @@ -30,9 +30,9 @@ db-up: db-down: $(DOCKER_COMPOSE) -f $(DOCKER_COMPOSE_FILE) down $(DOCKER_DB_CONTAINER_NAME) -.PHONY: db-init -db-init: - USERNAME="$(USERNAME)" PASSWORD="$(PASSWORD)" DATABASE_URL="$(DATABASE_URL)" DATABASE_INIT_FILE="$(DATABASE_INIT_FILE)" cargo run +.PHONY: db-migrate +db-migrate: + DATABASE_URL="$(DATABASE_URL)" diesel migration run # Clean up the Docker volume .PHONY: db-clean diff --git a/db_test.sql b/db_test.sql deleted file mode 100644 index 435452e..0000000 --- a/db_test.sql +++ /dev/null @@ -1,6 +0,0 @@ -DROP TABLE IF EXISTS contribution; -CREATE TABLE IF NOT EXISTS contribution -( - id bigint PRIMARY KEY NOT NULL, - created_at timestamp with time zone DEFAULT (now() at time zone 'utc') -); \ No newline at end of file diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..c028f4a --- /dev/null +++ b/diesel.toml @@ -0,0 +1,9 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId"] + +[migrations_directory] +dir = "migrations" diff --git a/migrations/.keep b/migrations/.keep new file mode 100644 index 0000000..e69de29 diff --git a/migrations/00000000000000_diesel_initial_setup/down.sql b/migrations/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 0000000..0c0ba38 --- /dev/null +++ b/migrations/00000000000000_diesel_initial_setup/down.sql @@ -0,0 +1,5 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. +DROP FUNCTION diesel_manage_updated_at(_tbl regclass); +DROP FUNCTION diesel_set_updated_at(); \ No newline at end of file diff --git a/migrations/00000000000000_diesel_initial_setup/up.sql b/migrations/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 0000000..d68895b --- /dev/null +++ b/migrations/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/migrations/2024-04-13-204151_init/down.sql b/migrations/2024-04-13-204151_init/down.sql new file mode 100644 index 0000000..c988c3e --- /dev/null +++ b/migrations/2024-04-13-204151_init/down.sql @@ -0,0 +1,23 @@ +-- Drop tables +DROP TABLE IF EXISTS comments; +DROP TABLE IF EXISTS wishes; +DROP TABLE IF EXISTS maintainers; +DROP TABLE IF EXISTS issues_labels; +DROP TABLE IF EXISTS repositories_filters; +DROP TABLE IF EXISTS repositories_topics; +DROP TABLE IF EXISTS repositories_languages; +DROP TABLE IF EXISTS issues; +DROP TABLE IF EXISTS tips; +DROP TABLE IF EXISTS repositories; +DROP TABLE IF EXISTS organizations; +DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS filter_values; +DROP TABLE IF EXISTS filters; +DROP TABLE IF EXISTS blockchains; +DROP TABLE IF EXISTS topics; +DROP TABLE IF EXISTS labels; +DROP TABLE IF EXISTS languages; +-- Drop enums +DROP TYPE IF EXISTS tip_status; +DROP TYPE IF EXISTS issue_status; +DROP TYPE IF EXISTS tip_type; \ No newline at end of file diff --git a/db.sql b/migrations/2024-04-13-204151_init/up.sql similarity index 83% rename from db.sql rename to migrations/2024-04-13-204151_init/up.sql index dd811e2..56db71a 100644 --- a/db.sql +++ b/migrations/2024-04-13-204151_init/up.sql @@ -2,76 +2,33 @@ CREATE TYPE tip_status AS ENUM ('set', 'paid', 'rejected'); CREATE TYPE issue_status AS ENUM ('open', 'closed'); CREATE TYPE tip_type AS ENUM ('direct', 'gov'); --- all the users including maintainers and admins -CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, - username VARCHAR(100) UNIQUE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') -); --- basic github organization -CREATE TABLE IF NOT EXISTS organizations ( +-- some tables to save metadata associated to the issues, repositories and tips +-- used in the repositories +CREATE TABLE IF NOT EXISTS languages ( id SERIAL PRIMARY KEY, name VARCHAR(255) UNIQUE, - icon VARCHAR(255), - description VARCHAR(255), - url VARCHAR(255), - email VARCHAR(255), - twitter VARCHAR(255), - e_tag VARCHAR(40) NOT NULL, - is_verified boolean, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), - updated_at TIMESTAMP NULL -); --- basic github repository -CREATE TABLE IF NOT EXISTS repositories ( - id SERIAL PRIMARY KEY, - name VARCHAR(255), - url VARCHAR(255), - icon VARCHAR(255), - e_tag VARCHAR(40) NOT NULL, - organization_id INT REFERENCES organizations(id), - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP NULL ); -- used in the issues CREATE TABLE IF NOT EXISTS labels ( id SERIAL PRIMARY KEY, name VARCHAR(255) UNIQUE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP NULL ); -- used in the repositories CREATE TABLE IF NOT EXISTS topics ( id SERIAL PRIMARY KEY, name VARCHAR(255) UNIQUE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP NULL ); -CREATE TABLE IF NOT EXISTS languages ( - id SERIAL PRIMARY KEY, - name VARCHAR(255) UNIQUE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), - updated_at TIMESTAMP NULL -); --- one repository can have multiple languages --- one language can have multiple repositories -CREATE TABLE IF NOT EXISTS repositories_languages ( - id SERIAL PRIMARY KEY, - repositories_id INT REFERENCES repositories(id), - languages_id INT REFERENCES languages(id) -); --- one repository can have multiple topics --- one topic can have multiple repositories -CREATE TABLE IF NOT EXISTS repositories_topics ( - id SERIAL PRIMARY KEY, - repositories_id INT REFERENCES repositories(id), - topics_id INT REFERENCES topics(id) -); -- used in the tips -CREATE TABLE IF NOT EXISTS blockchain ( +CREATE TABLE IF NOT EXISTS blockchains ( id SERIAL PRIMARY KEY, name VARCHAR(255) UNIQUE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP NULL ); -- filters used by the frontend: languages, interests, etc @@ -79,7 +36,7 @@ CREATE TABLE IF NOT EXISTS filters ( id SERIAL PRIMARY KEY, name VARCHAR(255) UNIQUE, emoji TEXT NOT NULL, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP NULL ); -- filters used by the frontend: EVM, documentation, etc. @@ -88,19 +45,39 @@ CREATE TABLE IF NOT EXISTS filter_values ( filters_id INT REFERENCES filters(id) NOT NULL, emoji TEXT NOT NULL, name VARCHAR(255) UNIQUE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP NULL ); --- we need to associate the repository (and then the issue) to some --- filters that are used by the frontend. --- For example, "Interests" -> "EVM" and "Network" -> "Kusama", "Polkadot" --- one repository can have multiple filters --- one topic can have multiple repositories -CREATE TABLE IF NOT EXISTS repositories_filters ( +-- basic github organization +CREATE TABLE IF NOT EXISTS organizations ( id SERIAL PRIMARY KEY, - repositories_id INT REFERENCES repositories(id), - filters_id INT REFERENCES filters(id), - filter_values_id INT REFERENCES filter_values(id) + name VARCHAR(255) UNIQUE, + icon VARCHAR(255), + description VARCHAR(255), + url VARCHAR(255), + email VARCHAR(255), + twitter VARCHAR(255), + e_tag VARCHAR(40) NOT NULL, + is_verified boolean, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, + updated_at TIMESTAMP NULL +); +-- basic github repository +CREATE TABLE IF NOT EXISTS repositories ( + id SERIAL PRIMARY KEY, + name VARCHAR(255), + url VARCHAR(255), + icon VARCHAR(255), + e_tag VARCHAR(40) NOT NULL, + organization_id INT REFERENCES organizations(id), + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, + updated_at TIMESTAMP NULL +); +-- all the users including maintainers and admins +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + username VARCHAR(100) UNIQUE NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL ); -- two types of tips: gov and direct. One tip per issue CREATE TABLE IF NOT EXISTS tips ( @@ -112,13 +89,13 @@ CREATE TABLE IF NOT EXISTS tips ( "from" VARCHAR(48) NOT NULL, --direct transaction VARCHAR(255) NULL, - blockchain_id INT REFERENCES blockchain(id) NULL, + blockchain_id INT REFERENCES blockchains(id) NULL, -- gov url VARCHAR(255) NULL, contributor_id INT REFERENCES users(id) NOT NULL, --not needed if it's a proposal curator_id INT REFERENCES users(id) NULL, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP NULL ); -- issue with repository, user and tip @@ -129,14 +106,40 @@ CREATE TABLE IF NOT EXISTS issues ( title VARCHAR(255), description VARCHAR(255), status issue_status NOT NULL, + has_wishes boolean, repository_id INT REFERENCES repositories(id) NOT NULL, user_id INT REFERENCES users(id) NULL, tip_id INT REFERENCES tips(id) NULL, issue_created_at TIMESTAMP NOT NULL, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, e_tag VARCHAR(40) NOT NULL, updated_at TIMESTAMP NULL ); +-- one repository can have multiple languages +-- one language can have multiple repositories +CREATE TABLE IF NOT EXISTS repositories_languages ( + id SERIAL PRIMARY KEY, + repositories_id INT REFERENCES repositories(id), + languages_id INT REFERENCES languages(id) +); +-- one repository can have multiple topics +-- one topic can have multiple repositories +CREATE TABLE IF NOT EXISTS repositories_topics ( + id SERIAL PRIMARY KEY, + repositories_id INT REFERENCES repositories(id), + topics_id INT REFERENCES topics(id) +); +-- we need to associate the repository (and then the issue) to some +-- filters that are used by the frontend. +-- For example, "Interests" -> "EVM" and "Network" -> "Kusama", "Polkadot" +-- one repository can have multiple filters +-- one topic can have multiple repositories +CREATE TABLE IF NOT EXISTS repositories_filters ( + id SERIAL PRIMARY KEY, + repositories_id INT REFERENCES repositories(id), + filters_id INT REFERENCES filters(id), + filter_values_id INT REFERENCES filter_values(id) +); -- one issue can have multiple labels -- one label can have multiple issues CREATE TABLE IF NOT EXISTS issues_labels ( @@ -151,19 +154,21 @@ CREATE TABLE IF NOT EXISTS maintainers ( repository_id INT REFERENCES repositories(id), user_id INT REFERENCES users(id) ); --- wishes are an special issue in some well known repositories +-- wishes are special issues where comments are fetched CREATE TABLE IF NOT EXISTS wishes ( id SERIAL PRIMARY KEY, - issue_number INT NOT NULL, - url VARCHAR(255), - title VARCHAR(255), - description VARCHAR(255), - status issue_status NOT NULL, - repository_id INT REFERENCES repositories(id) NOT NULL, - user_id INT REFERENCES users(id) NULL, - tip_id INT REFERENCES tips(id) NULL, - issue_created_at TIMESTAMP NOT NULL, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc'), - e_tag VARCHAR(40) NOT NULL, + issues_id INT REFERENCES issues(id) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, + updated_at TIMESTAMP NULL +); +-- each wish has multiple comments +CREATE TABLE IF NOT EXISTS comments ( + id SERIAL PRIMARY KEY, + wish_id INT REFERENCES wishes(id) NOT NULL, + user_id INT REFERENCES users(id) NOT NULL, + comment VARCHAR(255) UNIQUE, + positive_votes INT, + negative_votes INT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP NULL ); \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index fe2aefc..a7f9bfa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,15 +45,6 @@ async fn run() { .expect("Cannot create DB connection"); let db = DBAccess::new(db_pool); - // create db and exit - if database_init_file != "" { - db.init_db(&database_init_file) - .await - .expect("Cannot create DB scheme"); - print!("DB schem created"); - process::exit(0); - } - let health_route = health::routes::routes(db.clone()); let users_route = user::routes::routes(db.clone()); let organizations_route = organization::routes::routes(db.clone()); diff --git a/src/tests/contribution.rs b/src/tests/contribution.rs index 11b27e9..5ff5c10 100644 --- a/src/tests/contribution.rs +++ b/src/tests/contribution.rs @@ -1,493 +1,493 @@ -#[cfg(test)] -mod tests { - use crate::{ - handlers::{error_handler, ErrorResponse}, - init_db, - user::routes::routes, - user::{ - db::DBUser, - models::{User, UserRequest, UserResponse}, - }, - }; - use mobc::async_trait; - use warp::{reject, test::request, Filter}; - - #[derive(Clone)] - pub struct DBMockValues {} - #[derive(Clone)] - pub struct DBMockEmpty {} - - #[async_trait] - impl DBUser for DBMockValues { - async fn get_user(&self, id: i32) -> Result, reject::Rejection> { - Ok(Some(User { - id, - username: "username".to_string(), - })) - } - async fn get_user_by_username( - &self, - username: &str, - ) -> Result, reject::Rejection> { - Ok(Some(User { - id: 1, - username: username.to_string(), - })) - } - async fn get_users(&self) -> Result, reject::Rejection> { - Ok(vec![User { - id: 1, - username: "username".to_string(), - }]) - } - async fn create_user(&self, user: UserRequest) -> Result { - Ok(User { - id: 1, - username: user.username, - }) - } - async fn delete_user(&self, _: i32) -> Result<(), reject::Rejection> { - Ok(()) - } - } - #[async_trait] - impl DBUser for DBMockEmpty { - async fn get_user(&self, _: i32) -> Result, reject::Rejection> { - Ok(None) - } - async fn get_users(&self) -> Result, reject::Rejection> { - Ok(vec![]) - } - async fn get_user_by_username( - &self, - _username: &str, - ) -> Result, reject::Rejection> { - Ok(None) - } - async fn create_user(&self, user: UserRequest) -> Result { - Ok(User { - id: 1, - username: user.username, - }) - } - async fn delete_user(&self, _: i32) -> Result<(), reject::Rejection> { - Ok(()) - } - } - - #[tokio::test] - async fn test_get_user_mock_db() { - let id = 1; - let r = routes(DBMockValues {}); - let resp = request().path(&format!("/users/{id}")).reply(&r).await; - assert_eq!(resp.status(), 200); - let body = resp.into_body(); - assert!(!body.is_empty()); - let expected_response = UserResponse { - id, - username: "username".to_string(), - }; - let response: UserResponse = serde_json::from_slice(&body).unwrap(); - assert_eq!(response, expected_response) - } - #[tokio::test] - async fn test_get_user_not_found_mock_db() { - let id = 1; - let r = routes(DBMockEmpty {}).recover(error_handler); - let resp = request().path(&format!("/users/{id}")).reply(&r).await; - - assert_eq!(resp.status(), 404); - let body = resp.into_body(); - assert!(!body.is_empty()); - - let expected_response = ErrorResponse { - message: format!("User #{} not found", id), - }; - let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); - - assert_eq!(response, expected_response) - } - - #[tokio::test] - async fn test_get_users_mock_db() { - let r = routes(DBMockValues {}); - let resp = request().path(&format!("/users")).reply(&r).await; - - assert_eq!(resp.status(), 200); - let body = resp.into_body(); - assert!(!body.is_empty()); - let expected_response = vec![UserResponse { - id: 1, - username: "username".to_string(), - }]; - let response: Vec = serde_json::from_slice(&body).unwrap(); - assert_eq!(response, expected_response) - } - - #[tokio::test] - async fn test_get_users_empty_mock_db() { - let r = routes(DBMockEmpty {}); - let resp = request().path(&format!("/users")).reply(&r).await; - assert_eq!(resp.status(), 200); - - let body = resp.into_body(); - let response: Vec = serde_json::from_slice(&body).unwrap(); - let expected_response: Vec = vec![]; - assert_eq!(response, expected_response); - } - - #[tokio::test] - async fn test_create_user_mock_db() { - let id = 1; - let new_user = serde_json::to_vec(&UserResponse { - id: 1, - username: "username".to_string(), - }) - .unwrap(); - let r = routes(DBMockEmpty {}); - let resp = request() - .body(new_user) - .path(&"/users") - .method("POST") - .reply(&r) - .await; - assert_eq!(resp.status(), 200); - let body = resp.into_body(); - assert!(!body.is_empty()); - - let expected_response = UserResponse { - id, - username: "username".to_string(), - }; - let response: UserResponse = serde_json::from_slice(&body).unwrap(); - assert_eq!(response, expected_response) - } - - #[tokio::test] - async fn test_create_user_already_exists_mock_db() { - let id = 1; - let new_user = serde_json::to_vec(&UserResponse { - id: 1, - username: "username".to_string(), - }) - .unwrap(); - let r = routes(DBMockValues {}).recover(error_handler); - let resp = request() - .body(new_user) - .path(&"/users") - .method("POST") - .reply(&r) - .await; - assert_eq!(resp.status(), 400); - let body = resp.into_body(); - assert!(!body.is_empty()); - - let expected_response = ErrorResponse { - message: format!("User #{} already exists", id), - }; - - let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); - assert_eq!(response, expected_response) - } - - #[tokio::test] - async fn test_delete_user_mock_db() { - let id = 1; - let r = routes(DBMockValues {}); - let resp = request() - .path(&format!("/users/{id}")) - .method("DELETE") - .reply(&r) - .await; - assert_eq!(resp.status(), 200); - let body = resp.into_body(); - assert!(body.is_empty()); - } - - #[tokio::test] - async fn test_delete_user_does_not_exist_mock_db() { - let id = 1; - let new_user = serde_json::to_vec(&UserResponse { - id: 1, - username: "username".to_string(), - }) - .unwrap(); - let r = routes(DBMockEmpty {}).recover(error_handler); - let resp = request() - .body(new_user) - .path(&format!("/users/{id}")) - .method("DELETE") - .reply(&r) - .await; - - assert_eq!(resp.status(), 404); - let body = resp.into_body(); - assert!(!body.is_empty()); - - let expected_response = ErrorResponse { - message: format!("User #{} not found", id), - }; - let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); - assert_eq!(response, expected_response) - } - - #[tokio::test] - #[ignore] - async fn test_create_user_db() { - let id = 1; - let new_user = serde_json::to_vec(&UserResponse { - id: 1, - username: "username".to_string(), - }) - .unwrap(); - let db = init_db( - "postgres://postgres:password@localhost:5432/database".to_string(), - "db_test.sql".to_string(), - ) - .await - .unwrap(); - let r = routes(db); - let resp = request() - .body(new_user) - .path(&"/users") - .method("POST") - .reply(&r) - .await; - assert_eq!(resp.status(), 200); - let body = resp.into_body(); - assert!(!body.is_empty()); - - let expected_response = UserResponse { - id, - username: "username".to_string(), - }; - let response: UserResponse = serde_json::from_slice(&body).unwrap(); - assert_eq!(response, expected_response) - } - - #[tokio::test] - #[ignore] - async fn test_create_user_already_exists_db() { - let id = 1; - let new_user = serde_json::to_vec(&UserResponse { - id: 1, - username: "username".to_string(), - }) - .unwrap(); - let db = init_db( - "postgres://postgres:password@localhost:5432/database".to_string(), - "db_test.sql".to_string(), - ) - .await - .unwrap(); - let r = routes(db).recover(error_handler); - let _ = request() - .body(new_user.clone()) - .path(&"/users") - .method("POST") - .reply(&r) - .await; - let resp = request() - .body(new_user) - .path(&"/users") - .method("POST") - .reply(&r) - .await; - assert_eq!(resp.status(), 400); - let body = resp.into_body(); - assert!(!body.is_empty()); - - let expected_response = ErrorResponse { - message: format!("User #{} already exists", id), - }; - - let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); - assert_eq!(response, expected_response) - } - - #[tokio::test] - #[ignore] - async fn test_get_user_not_found_db() { - let id = 1; - let db = init_db( - "postgres://postgres:password@localhost:5432/database".to_string(), - "db_test.sql".to_string(), - ) - .await - .unwrap(); - let r = routes(db).recover(error_handler); - let resp = request().path(&format!("/users/{id}")).reply(&r).await; - - assert_eq!(resp.status(), 404); - let body = resp.into_body(); - assert!(!body.is_empty()); - - let expected_response = ErrorResponse { - message: "User #1 not found".to_string(), - }; - let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); - - assert_eq!(response, expected_response) - } - - #[tokio::test] - #[ignore] - async fn test_get_user_db() { - let id = 1; - let new_user = serde_json::to_vec(&UserResponse { - id: 1, - username: "username".to_string(), - }) - .unwrap(); - let db = init_db( - "postgres://postgres:password@localhost:5432/database".to_string(), - "db_test.sql".to_string(), - ) - .await - .unwrap(); - let r = routes(db); - let _ = request() - .body(new_user) - .path(&"/users") - .method("POST") - .reply(&r) - .await; - let resp = request().path(&format!("/users/{id}")).reply(&r).await; - // assert_eq!(resp.status(), 200); - let body = resp.into_body(); - assert!(!body.is_empty()); - let expected_response = UserResponse { - id, - username: "username".to_string(), - }; - let response: UserResponse = serde_json::from_slice(&body).unwrap(); - assert_eq!(response, expected_response) - } - - #[tokio::test] - #[ignore] - async fn test_get_users_empty_db() { - let db = init_db( - "postgres://postgres:password@localhost:5432/database".to_string(), - "db_test.sql".to_string(), - ) - .await - .unwrap(); - let r = routes(db).recover(error_handler); - let resp = request().path(&format!("/users")).reply(&r).await; - - assert_eq!(resp.status(), 200); - let body = resp.into_body(); - let response: Vec = serde_json::from_slice(&body).unwrap(); - let expected_response: Vec = vec![]; - assert_eq!(response, expected_response); - } - #[tokio::test] - #[ignore] - async fn test_get_users_db() { - let id = 1; - let new_user = serde_json::to_vec(&UserResponse { - id: 1, - username: "username".to_string(), - }) - .unwrap(); - let db = init_db( - "postgres://postgres:password@localhost:5432/database".to_string(), - "db_test.sql".to_string(), - ) - .await - .unwrap(); - let r = routes(db); - let _ = request() - .body(new_user) - .path(&"/users") - .method("POST") - .reply(&r) - .await; - let resp = request().path(&format!("/users")).reply(&r).await; - assert_eq!(resp.status(), 200); - - let body = resp.into_body(); - assert!(!body.is_empty()); - let expected_response = vec![UserResponse { - id, - username: "username".to_string(), - }]; - let response: Vec = serde_json::from_slice(&body).unwrap(); - assert_eq!(response, expected_response) - } - - #[tokio::test] - #[ignore] - async fn test_delete_user_db() { - let id = 1; - let new_user = serde_json::to_vec(&UserResponse { - id: 1, - username: "username".to_string(), - }) - .unwrap(); - let db = init_db( - "postgres://postgres:password@localhost:5432/database".to_string(), - "db_test.sql".to_string(), - ) - .await - .unwrap(); - let r = routes(db).recover(error_handler); - let resp = request() - .body(new_user) - .path(&"/users") - .method("POST") - .reply(&r) - .await; - assert_eq!(resp.status(), 200); - - let resp = request() - .path(&format!("/users/{id}")) - .method("DELETE") - .reply(&r) - .await; - assert_eq!(resp.status(), 200); - let body = resp.into_body(); - assert!(body.is_empty()); - - let resp = request().path(&format!("/users/{id}")).reply(&r).await; - - assert_eq!(resp.status(), 404); - let body = resp.into_body(); - assert!(!body.is_empty()); - } - - #[tokio::test] - #[ignore] - async fn test_delete_user_does_not_exist_db() { - let id = 1; - let db = init_db( - "postgres://postgres:password@localhost:5432/database".to_string(), - "db_test.sql".to_string(), - ) - .await - .unwrap(); - let new_user = serde_json::to_vec(&UserResponse { - id: 1, - username: "username".to_string(), - }) - .unwrap(); - let r = routes(db).recover(error_handler); - let resp = request() - .body(new_user) - .path(&format!("/users/{id}")) - .method("DELETE") - .reply(&r) - .await; - - assert_eq!(resp.status(), 404); - let body = resp.into_body(); - assert!(!body.is_empty()); - - let expected_response = ErrorResponse { - message: "User #1 not found".to_string(), - }; - let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); - assert_eq!(response, expected_response) - } -} +// #[cfg(test)] +// mod tests { +// use crate::{ +// handlers::{error_handler, ErrorResponse}, +// init_db, +// user::routes::routes, +// user::{ +// db::DBUser, +// models::{NewUser, User, UserResponse}, +// }, +// }; +// use mobc::async_trait; +// use warp::{reject, test::request, Filter}; + +// #[derive(Clone)] +// pub struct DBMockValues {} +// #[derive(Clone)] +// pub struct DBMockEmpty {} + +// #[async_trait] +// impl DBUser for DBMockValues { +// async fn get_user(&self, id: i32) -> Result, reject::Rejection> { +// Ok(Some(User { +// id, +// username: "username".to_string(), +// })) +// } +// async fn get_user_by_username( +// &self, +// username: &str, +// ) -> Result, reject::Rejection> { +// Ok(Some(User { +// id: 1, +// username: username.to_string(), +// })) +// } +// async fn get_users(&self) -> Result, reject::Rejection> { +// Ok(vec![User { +// id: 1, +// username: "username".to_string(), +// }]) +// } +// async fn create_user(&self, user: NewUser) -> Result { +// Ok(User { +// id: 1, +// username: user.username, +// }) +// } +// async fn delete_user(&self, _: i32) -> Result<(), reject::Rejection> { +// Ok(()) +// } +// } +// #[async_trait] +// impl DBUser for DBMockEmpty { +// async fn get_user(&self, _: i32) -> Result, reject::Rejection> { +// Ok(None) +// } +// async fn get_users(&self) -> Result, reject::Rejection> { +// Ok(vec![]) +// } +// async fn get_user_by_username( +// &self, +// _username: &str, +// ) -> Result, reject::Rejection> { +// Ok(None) +// } +// async fn create_user(&self, user: NewUser) -> Result { +// Ok(User { +// id: 1, +// username: user.username, +// }) +// } +// async fn delete_user(&self, _: i32) -> Result<(), reject::Rejection> { +// Ok(()) +// } +// } + +// #[tokio::test] +// async fn test_get_user_mock_db() { +// let id = 1; +// let r = routes(DBMockValues {}); +// let resp = request().path(&format!("/users/{id}")).reply(&r).await; +// assert_eq!(resp.status(), 200); +// let body = resp.into_body(); +// assert!(!body.is_empty()); +// let expected_response = UserResponse { +// id, +// username: "username".to_string(), +// }; +// let response: UserResponse = serde_json::from_slice(&body).unwrap(); +// assert_eq!(response, expected_response) +// } +// #[tokio::test] +// async fn test_get_user_not_found_mock_db() { +// let id = 1; +// let r = routes(DBMockEmpty {}).recover(error_handler); +// let resp = request().path(&format!("/users/{id}")).reply(&r).await; + +// assert_eq!(resp.status(), 404); +// let body = resp.into_body(); +// assert!(!body.is_empty()); + +// let expected_response = ErrorResponse { +// message: format!("User #{} not found", id), +// }; +// let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); + +// assert_eq!(response, expected_response) +// } + +// #[tokio::test] +// async fn test_get_users_mock_db() { +// let r = routes(DBMockValues {}); +// let resp = request().path(&format!("/users")).reply(&r).await; + +// assert_eq!(resp.status(), 200); +// let body = resp.into_body(); +// assert!(!body.is_empty()); +// let expected_response = vec![UserResponse { +// id: 1, +// username: "username".to_string(), +// }]; +// let response: Vec = serde_json::from_slice(&body).unwrap(); +// assert_eq!(response, expected_response) +// } + +// #[tokio::test] +// async fn test_get_users_empty_mock_db() { +// let r = routes(DBMockEmpty {}); +// let resp = request().path(&format!("/users")).reply(&r).await; +// assert_eq!(resp.status(), 200); + +// let body = resp.into_body(); +// let response: Vec = serde_json::from_slice(&body).unwrap(); +// let expected_response: Vec = vec![]; +// assert_eq!(response, expected_response); +// } + +// #[tokio::test] +// async fn test_create_user_mock_db() { +// let id = 1; +// let new_user = serde_json::to_vec(&UserResponse { +// id: 1, +// username: "username".to_string(), +// }) +// .unwrap(); +// let r = routes(DBMockEmpty {}); +// let resp = request() +// .body(new_user) +// .path(&"/users") +// .method("POST") +// .reply(&r) +// .await; +// assert_eq!(resp.status(), 200); +// let body = resp.into_body(); +// assert!(!body.is_empty()); + +// let expected_response = UserResponse { +// id, +// username: "username".to_string(), +// }; +// let response: UserResponse = serde_json::from_slice(&body).unwrap(); +// assert_eq!(response, expected_response) +// } + +// #[tokio::test] +// async fn test_create_user_already_exists_mock_db() { +// let id = 1; +// let new_user = serde_json::to_vec(&UserResponse { +// id: 1, +// username: "username".to_string(), +// }) +// .unwrap(); +// let r = routes(DBMockValues {}).recover(error_handler); +// let resp = request() +// .body(new_user) +// .path(&"/users") +// .method("POST") +// .reply(&r) +// .await; +// assert_eq!(resp.status(), 400); +// let body = resp.into_body(); +// assert!(!body.is_empty()); + +// let expected_response = ErrorResponse { +// message: format!("User #{} already exists", id), +// }; + +// let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); +// assert_eq!(response, expected_response) +// } + +// #[tokio::test] +// async fn test_delete_user_mock_db() { +// let id = 1; +// let r = routes(DBMockValues {}); +// let resp = request() +// .path(&format!("/users/{id}")) +// .method("DELETE") +// .reply(&r) +// .await; +// assert_eq!(resp.status(), 200); +// let body = resp.into_body(); +// assert!(body.is_empty()); +// } + +// #[tokio::test] +// async fn test_delete_user_does_not_exist_mock_db() { +// let id = 1; +// let new_user = serde_json::to_vec(&UserResponse { +// id: 1, +// username: "username".to_string(), +// }) +// .unwrap(); +// let r = routes(DBMockEmpty {}).recover(error_handler); +// let resp = request() +// .body(new_user) +// .path(&format!("/users/{id}")) +// .method("DELETE") +// .reply(&r) +// .await; + +// assert_eq!(resp.status(), 404); +// let body = resp.into_body(); +// assert!(!body.is_empty()); + +// let expected_response = ErrorResponse { +// message: format!("User #{} not found", id), +// }; +// let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); +// assert_eq!(response, expected_response) +// } + +// #[tokio::test] +// #[ignore] +// async fn test_create_user_db() { +// let id = 1; +// let new_user = serde_json::to_vec(&UserResponse { +// id: 1, +// username: "username".to_string(), +// }) +// .unwrap(); +// let db = init_db( +// "postgres://postgres:password@localhost:5432/database".to_string(), +// "db_test.sql".to_string(), +// ) +// .await +// .unwrap(); +// let r = routes(db); +// let resp = request() +// .body(new_user) +// .path(&"/users") +// .method("POST") +// .reply(&r) +// .await; +// assert_eq!(resp.status(), 200); +// let body = resp.into_body(); +// assert!(!body.is_empty()); + +// let expected_response = UserResponse { +// id, +// username: "username".to_string(), +// }; +// let response: UserResponse = serde_json::from_slice(&body).unwrap(); +// assert_eq!(response, expected_response) +// } + +// #[tokio::test] +// #[ignore] +// async fn test_create_user_already_exists_db() { +// let id = 1; +// let new_user = serde_json::to_vec(&UserResponse { +// id: 1, +// username: "username".to_string(), +// }) +// .unwrap(); +// let db = init_db( +// "postgres://postgres:password@localhost:5432/database".to_string(), +// "db_test.sql".to_string(), +// ) +// .await +// .unwrap(); +// let r = routes(db).recover(error_handler); +// let _ = request() +// .body(new_user.clone()) +// .path(&"/users") +// .method("POST") +// .reply(&r) +// .await; +// let resp = request() +// .body(new_user) +// .path(&"/users") +// .method("POST") +// .reply(&r) +// .await; +// assert_eq!(resp.status(), 400); +// let body = resp.into_body(); +// assert!(!body.is_empty()); + +// let expected_response = ErrorResponse { +// message: format!("User #{} already exists", id), +// }; + +// let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); +// assert_eq!(response, expected_response) +// } + +// #[tokio::test] +// #[ignore] +// async fn test_get_user_not_found_db() { +// let id = 1; +// let db = init_db( +// "postgres://postgres:password@localhost:5432/database".to_string(), +// "db_test.sql".to_string(), +// ) +// .await +// .unwrap(); +// let r = routes(db).recover(error_handler); +// let resp = request().path(&format!("/users/{id}")).reply(&r).await; + +// assert_eq!(resp.status(), 404); +// let body = resp.into_body(); +// assert!(!body.is_empty()); + +// let expected_response = ErrorResponse { +// message: "User #1 not found".to_string(), +// }; +// let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); + +// assert_eq!(response, expected_response) +// } + +// #[tokio::test] +// #[ignore] +// async fn test_get_user_db() { +// let id = 1; +// let new_user = serde_json::to_vec(&UserResponse { +// id: 1, +// username: "username".to_string(), +// }) +// .unwrap(); +// let db = init_db( +// "postgres://postgres:password@localhost:5432/database".to_string(), +// "db_test.sql".to_string(), +// ) +// .await +// .unwrap(); +// let r = routes(db); +// let _ = request() +// .body(new_user) +// .path(&"/users") +// .method("POST") +// .reply(&r) +// .await; +// let resp = request().path(&format!("/users/{id}")).reply(&r).await; +// // assert_eq!(resp.status(), 200); +// let body = resp.into_body(); +// assert!(!body.is_empty()); +// let expected_response = UserResponse { +// id, +// username: "username".to_string(), +// }; +// let response: UserResponse = serde_json::from_slice(&body).unwrap(); +// assert_eq!(response, expected_response) +// } + +// #[tokio::test] +// #[ignore] +// async fn test_get_users_empty_db() { +// let db = init_db( +// "postgres://postgres:password@localhost:5432/database".to_string(), +// "db_test.sql".to_string(), +// ) +// .await +// .unwrap(); +// let r = routes(db).recover(error_handler); +// let resp = request().path(&format!("/users")).reply(&r).await; + +// assert_eq!(resp.status(), 200); +// let body = resp.into_body(); +// let response: Vec = serde_json::from_slice(&body).unwrap(); +// let expected_response: Vec = vec![]; +// assert_eq!(response, expected_response); +// } +// #[tokio::test] +// #[ignore] +// async fn test_get_users_db() { +// let id = 1; +// let new_user = serde_json::to_vec(&UserResponse { +// id: 1, +// username: "username".to_string(), +// }) +// .unwrap(); +// let db = init_db( +// "postgres://postgres:password@localhost:5432/database".to_string(), +// "db_test.sql".to_string(), +// ) +// .await +// .unwrap(); +// let r = routes(db); +// let _ = request() +// .body(new_user) +// .path(&"/users") +// .method("POST") +// .reply(&r) +// .await; +// let resp = request().path(&format!("/users")).reply(&r).await; +// assert_eq!(resp.status(), 200); + +// let body = resp.into_body(); +// assert!(!body.is_empty()); +// let expected_response = vec![UserResponse { +// id, +// username: "username".to_string(), +// }]; +// let response: Vec = serde_json::from_slice(&body).unwrap(); +// assert_eq!(response, expected_response) +// } + +// #[tokio::test] +// #[ignore] +// async fn test_delete_user_db() { +// let id = 1; +// let new_user = serde_json::to_vec(&UserResponse { +// id: 1, +// username: "username".to_string(), +// }) +// .unwrap(); +// let db = init_db( +// "postgres://postgres:password@localhost:5432/database".to_string(), +// "db_test.sql".to_string(), +// ) +// .await +// .unwrap(); +// let r = routes(db).recover(error_handler); +// let resp = request() +// .body(new_user) +// .path(&"/users") +// .method("POST") +// .reply(&r) +// .await; +// assert_eq!(resp.status(), 200); + +// let resp = request() +// .path(&format!("/users/{id}")) +// .method("DELETE") +// .reply(&r) +// .await; +// assert_eq!(resp.status(), 200); +// let body = resp.into_body(); +// assert!(body.is_empty()); + +// let resp = request().path(&format!("/users/{id}")).reply(&r).await; + +// assert_eq!(resp.status(), 404); +// let body = resp.into_body(); +// assert!(!body.is_empty()); +// } + +// #[tokio::test] +// #[ignore] +// async fn test_delete_user_does_not_exist_db() { +// let id = 1; +// let db = init_db( +// "postgres://postgres:password@localhost:5432/database".to_string(), +// "db_test.sql".to_string(), +// ) +// .await +// .unwrap(); +// let new_user = serde_json::to_vec(&UserResponse { +// id: 1, +// username: "username".to_string(), +// }) +// .unwrap(); +// let r = routes(db).recover(error_handler); +// let resp = request() +// .body(new_user) +// .path(&format!("/users/{id}")) +// .method("DELETE") +// .reply(&r) +// .await; + +// assert_eq!(resp.status(), 404); +// let body = resp.into_body(); +// assert!(!body.is_empty()); + +// let expected_response = ErrorResponse { +// message: "User #1 not found".to_string(), +// }; +// let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); +// assert_eq!(response, expected_response) +// } +// } diff --git a/src/tests/health.rs b/src/tests/health.rs index 2b57128..819c908 100644 --- a/src/tests/health.rs +++ b/src/tests/health.rs @@ -1,9 +1,6 @@ #[cfg(test)] mod tests { - use crate::{ - health::{db::DBHealth, routes::routes}, - init_db, - }; + use crate::health::{db::DBHealth, routes::routes}; use mobc::async_trait; use warp::{reject, test::request}; @@ -24,22 +21,22 @@ mod tests { assert_eq!(resp.status(), 200); assert!(resp.body().is_empty()); } + // TODO: fix + // #[tokio::test] + // #[ignore] + // async fn test_health_db() { + // let db = init_db( + // "postgres://postgres:password@localhost:5432/database".to_string(), + // "db.sql".to_string(), + // ) + // .await + // .unwrap(); - #[tokio::test] - #[ignore] - async fn test_health_db() { - let db = init_db( - "postgres://postgres:password@localhost:5432/database".to_string(), - "db.sql".to_string(), - ) - .await - .unwrap(); - - let r = routes(db); - let resp = request().path("/health").reply(&r).await; - assert_eq!(resp.status(), 200); - assert!(resp.body().is_empty()); - } + // let r = routes(db); + // let resp = request().path("/health").reply(&r).await; + // assert_eq!(resp.status(), 200); + // assert!(resp.body().is_empty()); + // } } // TODO: add e2e test using a real http server. diff --git a/src/user/db.rs b/src/user/db.rs index 7087d6d..0cc829e 100644 --- a/src/user/db.rs +++ b/src/user/db.rs @@ -1,33 +1,58 @@ +use diesel::RunQueryDsl; use mobc::async_trait; use mobc_postgres::tokio_postgres::Row; use warp::reject; use crate::db::{ - pool::DBAccess, + pool::{DBAccess, DBAccessor}, utils::{ execute_query_with_timeout, query_one_timeout, query_opt_timeout, query_with_timeout, DB_QUERY_TIMEOUT, }, }; -use super::models::{User, UserRequest}; +use super::models::{NewUser, User, UsersRelations}; const TABLE: &str = "users"; #[async_trait] pub trait DBUser: Send + Sync + Clone + 'static { - async fn get_user(&self, id: i32) -> Result, reject::Rejection>; + async fn get_user( + &self, + id: i32, + relations: UsersRelations, + ) -> Result, reject::Rejection>; async fn get_user_by_username(&self, username: &str) -> Result, reject::Rejection>; async fn get_users(&self) -> Result, reject::Rejection>; - async fn create_user(&self, user: UserRequest) -> Result; + async fn create_user(&self, user: NewUser) -> Result; async fn delete_user(&self, id: i32) -> Result<(), reject::Rejection>; } #[async_trait] impl DBUser for DBAccess { - async fn get_user(&self, id: i32) -> Result, reject::Rejection> { - let query = format!("SELECT * FROM {} WHERE id = $1", TABLE); + async fn get_user( + &self, + id: i32, + relations: UsersRelations, + ) -> Result, reject::Rejection> { + let mut query = format!("SELECT * FROM {} ", TABLE); + if relations.maintainers { + query += "LEFT JOIN maintainers on maintainers.user_id = users.id "; + query += "LEFT JOIN repositories on maintainers.repository_id = repositories.id "; + } + if relations.issues { + query += "LEFT JOIN issues on issues.user_id = users.id "; + if relations.tips { + query += "LEFT JOIN tips on tips.id = issues.id "; + } + } + if relations.wishes { + query += "LEFT JOIN comments on comments.user_id = users.id "; + query += "LEFT JOIN wishes on wishes.id = comments.wish_id "; + } + query += "WHERE id = $1"; + match query_opt_timeout(self, query.as_str(), &[&id], DB_QUERY_TIMEOUT).await? { Some(user) => Ok(Some(row_to_user(&user))), None => Ok(None), @@ -50,7 +75,7 @@ impl DBUser for DBAccess { Ok(rows.iter().map(row_to_user).collect()) } - async fn create_user(&self, user: UserRequest) -> Result { + async fn create_user(&self, user: NewUser) -> Result { let query = format!("INSERT INTO {} (username) VALUES ($1) RETURNING *", TABLE); let row = query_one_timeout(self, &query, &[&user.username], DB_QUERY_TIMEOUT).await?; Ok(row_to_user(&row)) diff --git a/src/user/handlers.rs b/src/user/handlers.rs index 9ce4d1a..e27bc7d 100644 --- a/src/user/handlers.rs +++ b/src/user/handlers.rs @@ -7,11 +7,11 @@ use warp::{ use super::{ db::DBUser, errors::UserError, - models::{UserRequest, UserResponse}, + models::{GetUserQuery, NewUser, UserResponse, UsersRelations}, }; pub async fn create_user_handler( - body: UserRequest, + body: NewUser, db_access: impl DBUser, ) -> Result { match db_access.get_user_by_username(&body.username).await? { @@ -20,8 +20,19 @@ pub async fn create_user_handler( } } -pub async fn get_user_handler(id: i32, db_access: impl DBUser) -> Result { - match db_access.get_user(id).await? { +pub async fn get_user_handler( + id: i32, + db_access: impl DBUser, + query: GetUserQuery, +) -> Result { + let relations = UsersRelations { + wishes: query.wishes.unwrap_or_default(), + tips: query.tips.unwrap_or_default(), + maintainers: query.maintainers.unwrap_or_default(), + issues: query.issues.unwrap_or_default(), + }; + + match db_access.get_user(id, relations).await? { None => Err(warp::reject::custom(UserError::UserNotFound(id)))?, Some(user) => Ok(json(&UserResponse::of(user))), } @@ -35,7 +46,7 @@ pub async fn get_users_handler(db_access: impl DBUser) -> Result Result { - match db_access.get_user(id).await? { + match db_access.get_user(id, UsersRelations::default()).await? { Some(_) => { let _ = &db_access.delete_user(id).await?; Ok(StatusCode::OK) diff --git a/src/user/models.rs b/src/user/models.rs index 20e192d..9cfa202 100644 --- a/src/user/models.rs +++ b/src/user/models.rs @@ -7,7 +7,7 @@ pub struct User { } #[derive(Serialize, Deserialize)] -pub struct UserRequest { +pub struct NewUser { pub username: String, } @@ -25,3 +25,51 @@ impl UserResponse { } } } + +#[derive(Serialize, Deserialize)] +pub enum UserSort { + ById, + ByUsername, + ByCreatedAt, +} + +impl UserSort { + pub fn to_string(&self) -> &str { + match self { + UserSort::ById => "id", + UserSort::ByUsername => "username", + UserSort::ByCreatedAt => "created_at", + } + } +} +impl Default for UsersQuery { + fn default() -> Self { + UsersQuery { + limit: Some(10), + offset: Some(0), // sort: UserSort::ById, + } + } +} +#[derive(Default)] +pub struct UsersRelations { + pub wishes: bool, + pub tips: bool, + pub maintainers: bool, + pub issues: bool, +} +// query args + +#[derive(Serialize, Deserialize, Default)] +pub struct GetUserQuery { + pub wishes: Option, + pub tips: Option, + pub maintainers: Option, + pub issues: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct UsersQuery { + limit: Option, + offset: Option, + // sort: UserSort, +} diff --git a/src/user/routes.rs b/src/user/routes.rs index c6803e3..a944177 100644 --- a/src/user/routes.rs +++ b/src/user/routes.rs @@ -1,5 +1,6 @@ use std::convert::Infallible; +use serde::{Deserialize, Serialize}; use warp::filters::BoxedFilter; use warp::{Filter, Reply}; @@ -7,6 +8,7 @@ use crate::auth::with_auth; use super::db::DBUser; use super::handlers; +use super::models::GetUserQuery; fn with_db( db_pool: impl DBUser, @@ -26,6 +28,7 @@ pub fn routes(db_access: impl DBUser) -> BoxedFilter<(impl Reply,)> { let get_user = user_id .and(warp::get()) .and(with_db(db_access.clone())) + .and(warp::query::()) .and_then(handlers::get_user_handler); let create_user = user @@ -41,7 +44,7 @@ pub fn routes(db_access: impl DBUser) -> BoxedFilter<(impl Reply,)> { .and(with_db(db_access.clone())) .and_then(handlers::delete_user_handler); - let route = get_users.or(get_user).or(create_user).or(delete_user); + let route = get_users.or(create_user).or(get_user).or(delete_user); route.boxed() } From ff7d3ddf92011a5adf9f7aacbabe4cca2f6ac7b0 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 14 Apr 2024 12:06:55 -0300 Subject: [PATCH 07/98] feat: add some custom query args in get users --- src/db/utils.rs | 8 ++++- src/user/db.rs | 74 ++++++++++++++++++++++++++++++++++++++------ src/user/errors.rs | 5 +++ src/user/handlers.rs | 56 ++++++++++++++++++++++++++++++--- src/user/models.rs | 58 ++++++++++++++++++++++++++++------ src/user/routes.rs | 19 ++++++++++-- 6 files changed, 192 insertions(+), 28 deletions(-) diff --git a/src/db/utils.rs b/src/db/utils.rs index cdf004e..8761c87 100644 --- a/src/db/utils.rs +++ b/src/db/utils.rs @@ -15,7 +15,13 @@ pub async fn query_with_timeout( timeout_duration: Duration, ) -> Result, reject::Rejection> { let db_conn = db_access.get_db_con().await.map_err(reject::custom)?; - + println!("{}", query); + println!("{:#?}", params); + let r = db_conn.query(query, params).await; + if r.is_err() { + let a = r.err().unwrap(); + println!("{}", a); + } timeout(timeout_duration, db_conn.query(query, params)) .await .map_err(|err| reject::custom(DBError::DBTimeout(err)))? diff --git a/src/user/db.rs b/src/user/db.rs index 0cc829e..88a80d0 100644 --- a/src/user/db.rs +++ b/src/user/db.rs @@ -1,17 +1,16 @@ -use diesel::RunQueryDsl; use mobc::async_trait; use mobc_postgres::tokio_postgres::Row; use warp::reject; use crate::db::{ - pool::{DBAccess, DBAccessor}, + pool::DBAccess, utils::{ execute_query_with_timeout, query_one_timeout, query_opt_timeout, query_with_timeout, DB_QUERY_TIMEOUT, }, }; -use super::models::{NewUser, User, UsersRelations}; +use super::models::{GetUsersFilters, NewUser, User, UsersFilters, UsersRelations}; const TABLE: &str = "users"; @@ -22,9 +21,16 @@ pub trait DBUser: Send + Sync + Clone + 'static { id: i32, relations: UsersRelations, ) -> Result, reject::Rejection>; - async fn get_user_by_username(&self, username: &str) - -> Result, reject::Rejection>; - async fn get_users(&self) -> Result, reject::Rejection>; + async fn get_user_by_username( + &self, + username: &str, + relations: UsersRelations, + ) -> Result, reject::Rejection>; + async fn get_users( + &self, + relations: UsersRelations, + filters: UsersFilters, + ) -> Result, reject::Rejection>; async fn create_user(&self, user: NewUser) -> Result; async fn delete_user(&self, id: i32) -> Result<(), reject::Rejection>; } @@ -61,17 +67,65 @@ impl DBUser for DBAccess { async fn get_user_by_username( &self, username: &str, + relations: UsersRelations, ) -> Result, reject::Rejection> { - let query = format!("SELECT * FROM {} WHERE username = $1", TABLE); + let mut query = format!("SELECT * FROM {} ", TABLE); + if relations.maintainers { + query += "LEFT JOIN maintainers on maintainers.user_id = users.id "; + query += "LEFT JOIN repositories on maintainers.repository_id = repositories.id "; + } + if relations.issues { + query += "LEFT JOIN issues on issues.user_id = users.id "; + if relations.tips { + query += "LEFT JOIN tips on tips.id = issues.id "; + } + } + if relations.wishes { + query += "LEFT JOIN comments on comments.user_id = users.id "; + query += "LEFT JOIN wishes on wishes.id = comments.wish_id "; + } + query += "WHERE username = $1"; match query_opt_timeout(self, query.as_str(), &[&username], DB_QUERY_TIMEOUT).await? { Some(user) => Ok(Some(row_to_user(&user))), None => Ok(None), } } - async fn get_users(&self) -> Result, reject::Rejection> { - let query = format!("SELECT * FROM {} ORDER BY created_at DESC", TABLE); - let rows = query_with_timeout(self, query.as_str(), &[], DB_QUERY_TIMEOUT).await?; + async fn get_users( + &self, + relations: UsersRelations, + filters: UsersFilters, + ) -> Result, reject::Rejection> { + let mut query = format!("SELECT * FROM {} ", TABLE); + if relations.maintainers { + query += "LEFT JOIN maintainers on maintainers.user_id = users.id "; + query += "LEFT JOIN repositories on maintainers.repository_id = repositories.id "; + } + if relations.issues { + query += "LEFT JOIN issues on issues.user_id = users.id "; + if relations.tips { + query += "LEFT JOIN tips on tips.id = issues.id "; + } + } + if relations.wishes { + query += "LEFT JOIN comments on comments.user_id = users.id "; + query += "LEFT JOIN wishes on wishes.id = comments.wish_id "; + } + // TODO: fix: ASC + query += "ORDER BY $1 ASC "; + query += "LIMIT $2 OFFSET $3"; + let rows = query_with_timeout( + self, + query.as_str(), + &[ + &filters.sort, + // &filters.ascending, + &filters.limit, + &filters.offset, + ], + DB_QUERY_TIMEOUT, + ) + .await?; Ok(rows.iter().map(row_to_user).collect()) } diff --git a/src/user/errors.rs b/src/user/errors.rs index 477d495..658a5c0 100644 --- a/src/user/errors.rs +++ b/src/user/errors.rs @@ -14,6 +14,7 @@ use crate::handlers::ErrorResponse; pub enum UserError { UserExists(i32), UserNotFound(i32), + UserNotFoundByName(String), } impl fmt::Display for UserError { @@ -25,6 +26,9 @@ impl fmt::Display for UserError { UserError::UserNotFound(id) => { write!(f, "User #{} not found", id) } + UserError::UserNotFoundByName(name) => { + write!(f, "User {} not found", name) + } } } } @@ -36,6 +40,7 @@ impl Reply for UserError { let code = match self { UserError::UserExists(_) => StatusCode::BAD_REQUEST, UserError::UserNotFound(_) => StatusCode::NOT_FOUND, + UserError::UserNotFoundByName(_) => StatusCode::NOT_FOUND, }; let message = self.to_string(); diff --git a/src/user/handlers.rs b/src/user/handlers.rs index e27bc7d..1e3953c 100644 --- a/src/user/handlers.rs +++ b/src/user/handlers.rs @@ -7,14 +7,17 @@ use warp::{ use super::{ db::DBUser, errors::UserError, - models::{GetUserQuery, NewUser, UserResponse, UsersRelations}, + models::{GetUserQuery, GetUsersFilters, NewUser, UserResponse, UsersFilters, UsersRelations}, }; pub async fn create_user_handler( body: NewUser, db_access: impl DBUser, ) -> Result { - match db_access.get_user_by_username(&body.username).await? { + match db_access + .get_user_by_username(&body.username, UsersRelations::default()) + .await? + { Some(u) => Err(warp::reject::custom(UserError::UserExists(u.id)))?, None => Ok(json(&UserResponse::of(db_access.create_user(body).await?))), } @@ -38,8 +41,53 @@ pub async fn get_user_handler( } } -pub async fn get_users_handler(db_access: impl DBUser) -> Result { - let users = db_access.get_users().await?; +pub async fn get_user_by_name_handler( + name: String, + db_access: impl DBUser, + query: GetUserQuery, +) -> Result { + let relations = UsersRelations { + wishes: query.wishes.unwrap_or_default(), + tips: query.tips.unwrap_or_default(), + maintainers: query.maintainers.unwrap_or_default(), + issues: query.issues.unwrap_or_default(), + }; + + match db_access.get_user_by_username(&name, relations).await? { + None => Err(warp::reject::custom(UserError::UserNotFoundByName(name)))?, + Some(user) => Ok(json(&UserResponse::of(user))), + } +} + +pub async fn get_users_handler( + db_access: impl DBUser, + query: GetUserQuery, + filters: GetUsersFilters, +) -> Result { + let relations = UsersRelations { + wishes: query.wishes.unwrap_or_default(), + tips: query.tips.unwrap_or_default(), + maintainers: query.maintainers.unwrap_or_default(), + issues: query.issues.unwrap_or_default(), + }; + // TODO: validate filters (sort) + let default_filters = filters.apply_defaults(); + // TODO: create generic Pagination + // TODO: Improve asc/desc + let asc = { + if default_filters.ascending.unwrap() { + "ASC" + } else { + "DESC" + } + }; + let filters = UsersFilters { + limit: default_filters.limit.unwrap(), + offset: default_filters.offset.unwrap(), + sort: default_filters.sort.unwrap().to_string().into(), + ascending: asc.to_string(), + }; + let users = db_access.get_users(relations, filters).await?; Ok(json::>( &users.into_iter().map(UserResponse::of).collect(), )) diff --git a/src/user/models.rs b/src/user/models.rs index 9cfa202..9078390 100644 --- a/src/user/models.rs +++ b/src/user/models.rs @@ -1,3 +1,5 @@ +use std::default; + use serde_derive::{Deserialize, Serialize}; #[derive(Deserialize)] @@ -26,8 +28,9 @@ impl UserResponse { } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Default, Clone)] pub enum UserSort { + #[default] ById, ByUsername, ByCreatedAt, @@ -42,11 +45,13 @@ impl UserSort { } } } -impl Default for UsersQuery { +impl Default for GetUsersFilters { fn default() -> Self { - UsersQuery { - limit: Some(10), - offset: Some(0), // sort: UserSort::ById, + GetUsersFilters { + limit: Some(1000), + offset: Some(0), + sort: Some("users.id".to_string()), + ascending: Some(true), } } } @@ -57,6 +62,12 @@ pub struct UsersRelations { pub maintainers: bool, pub issues: bool, } +pub struct UsersFilters { + pub limit: i64, + pub offset: i64, + pub sort: String, + pub ascending: String, +} // query args #[derive(Serialize, Deserialize, Default)] @@ -67,9 +78,36 @@ pub struct GetUserQuery { pub issues: Option, } -#[derive(Serialize, Deserialize)] -pub struct UsersQuery { - limit: Option, - offset: Option, - // sort: UserSort, +#[derive(Serialize, Deserialize, Clone)] +pub struct GetUsersFilters { + pub limit: Option, + pub offset: Option, + pub sort: Option, + pub ascending: Option, +} + +impl GetUsersFilters { + // A method to create an instance of GetUsersFilters with default values + pub fn new() -> Self { + GetUsersFilters::default() + } + + // A method to set default values for individual fields if they are None + pub fn apply_defaults(&self) -> Self { + let mut filters = self.clone(); + let default = Self::default(); + if self.limit.is_none() { + filters.limit = default.limit; + } + if self.offset.is_none() { + filters.offset = default.offset; + } + if self.sort.is_none() { + filters.sort = default.sort; + } + if self.ascending.is_none() { + filters.ascending = default.ascending; + } + filters + } } diff --git a/src/user/routes.rs b/src/user/routes.rs index a944177..6944648 100644 --- a/src/user/routes.rs +++ b/src/user/routes.rs @@ -8,7 +8,7 @@ use crate::auth::with_auth; use super::db::DBUser; use super::handlers; -use super::models::GetUserQuery; +use super::models::{GetUserQuery, GetUsersFilters}; fn with_db( db_pool: impl DBUser, @@ -19,10 +19,13 @@ fn with_db( pub fn routes(db_access: impl DBUser) -> BoxedFilter<(impl Reply,)> { let user = warp::path!("users"); let user_id = warp::path!("users" / i32); + let user_name = warp::path!("users" / String); let get_users = user .and(warp::get()) .and(with_db(db_access.clone())) + .and(warp::query::()) + .and(warp::query::()) .and_then(handlers::get_users_handler); let get_user = user_id @@ -31,20 +34,30 @@ pub fn routes(db_access: impl DBUser) -> BoxedFilter<(impl Reply,)> { .and(warp::query::()) .and_then(handlers::get_user_handler); + let get_user_by_name = user_name + .and(warp::get()) + .and(with_db(db_access.clone())) + .and(warp::query::()) + .and_then(handlers::get_user_by_name_handler); + // TODO: add maintainers let create_user = user .and(with_auth()) .and(warp::post()) .and(warp::body::json()) .and(with_db(db_access.clone())) .and_then(handlers::create_user_handler); - + // TODO: add PATCH maintainers let delete_user = user_id .and(with_auth()) .and(warp::delete()) .and(with_db(db_access.clone())) .and_then(handlers::delete_user_handler); - let route = get_users.or(create_user).or(get_user).or(delete_user); + let route = get_users + .or(create_user) + .or(get_user) + .or(delete_user) + .or(get_user_by_name); route.boxed() } From 0f729830c6a0b2897db5fc72b29d8fb0f858605c Mon Sep 17 00:00:00 2001 From: Leandro Date: Mon, 15 Apr 2024 10:57:09 -0300 Subject: [PATCH 08/98] feat: improve pagination and sort --- src/db/utils.rs | 16 +---- src/http.rs | 162 ++++++++++++++++++++++++++++++++++++++++++ src/issue/db.rs | 1 - src/issue/handlers.rs | 4 +- src/main.rs | 4 +- src/user/db.rs | 35 ++++----- src/user/handlers.rs | 36 +++++----- src/user/models.rs | 99 ++++++++++---------------- src/user/routes.rs | 7 +- 9 files changed, 244 insertions(+), 120 deletions(-) create mode 100644 src/http.rs diff --git a/src/db/utils.rs b/src/db/utils.rs index 8761c87..8558117 100644 --- a/src/db/utils.rs +++ b/src/db/utils.rs @@ -1,7 +1,4 @@ -use super::{ - errors::DBError, - pool::{self, DBAccess, DBAccessor}, -}; +use super::{errors::DBError, pool::DBAccessor}; use mobc_postgres::tokio_postgres::{types::ToSql, Row}; use tokio::time::{timeout, Duration}; use warp::reject; @@ -70,14 +67,3 @@ pub async fn query_one_timeout( .map_err(|err| reject::custom(DBError::DBTimeout(err)))? .map_err(|err| reject::custom(DBError::DBQuery(err))) } - -pub async fn init_db( - database_url: String, - database_init_file: String, -) -> Result { - let db_pool = pool::create_pool(&database_url).map_err(DBError::DBPoolConnection)?; - let db = DBAccess::new(db_pool); - // TODO: use migrations - db.init_db(&database_init_file).await?; - Ok(db) -} diff --git a/src/http.rs b/src/http.rs new file mode 100644 index 0000000..51b119e --- /dev/null +++ b/src/http.rs @@ -0,0 +1,162 @@ +use std::fmt; + +use thiserror::Error; +use warp::{ + http::StatusCode, + reject::Reject, + reply::{Reply, Response}, +}; + +use crate::handlers::ErrorResponse; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone)] +pub struct GetPagination { + pub limit: Option, + pub offset: Option, + // pub sort: Option, + // pub ascending: Option, +} + +impl GetPagination { + // A method to create an instance of GetPagination with default values + pub fn new() -> Self { + GetPagination::default() + } + + // A method to set default values for individual fields if they are None + pub fn validate(&self) -> Result { + let mut filters = self.clone(); + let default = Self::default(); + + if self.limit.is_none() { + filters.limit = default.limit; + } else { + let limit = self.limit.unwrap(); + if limit <= 0 || limit >= 1000 { + return Err(PaginationError::InvalidLimit(limit)); + } + } + if self.offset.is_none() { + filters.offset = default.offset; + } else { + let offset = self.offset.unwrap(); + if offset <= 0 { + return Err(PaginationError::InvalidOffset(offset)); + } + } + // if self.sort.is_none() { + // filters.sort = default.sort; + // } + // if self.ascending.is_none() { + // filters.ascending = default.ascending; + // } + Ok(filters) + } +} + +impl Default for GetPagination { + fn default() -> Self { + GetPagination { + limit: Some(1000), + offset: Some(0), + // sort: Some("users.id".to_string()), + // ascending: Some(true), + } + } +} + +#[derive(Clone, Error, Debug, Deserialize, PartialEq)] +pub enum PaginationError { + InvalidOffset(i64), + InvalidLimit(i64), +} + +impl fmt::Display for PaginationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PaginationError::InvalidOffset(offset) => { + write!(f, "Offset {} is invalid", offset) + } + PaginationError::InvalidLimit(limit) => { + write!(f, "Limit #{} not found", limit) + } + } + } +} + +impl Reject for PaginationError {} + +impl Reply for PaginationError { + fn into_response(self) -> Response { + let code = match self { + PaginationError::InvalidOffset(_) => StatusCode::BAD_REQUEST, + PaginationError::InvalidLimit(_) => StatusCode::BAD_REQUEST, + }; + let message = self.to_string(); + + let json = warp::reply::json(&ErrorResponse { message }); + + warp::reply::with_status(json, code).into_response() + } +} + +#[derive(Serialize, Deserialize, Default, Clone)] +pub struct GetSort { + pub sort_by: Option, + pub descending: Option, +} + +#[derive(Clone, Error, Debug, Deserialize, PartialEq)] +pub enum SortError { + InvalidSortBy(String), +} + +impl GetSort { + pub fn validate(&self, valid_fields: Vec<&str>) -> Result { + match (self.sort_by.clone(), self.descending) { + (None, None) => Ok(self.clone()), + (None, Some(_)) => Ok(Self { + sort_by: None, + descending: None, + }), + (Some(_), None) => Ok(Self { + sort_by: None, + descending: Some(false), + }), + (Some(sort_by), Some(_)) => { + //TODO: improve with trim, remove unexpected chars, etc. + if valid_fields.iter().any(|&s| s == sort_by) { + Ok(self.clone()) + } else { + Err(SortError::InvalidSortBy(sort_by)) + } + } + } + } +} + +impl fmt::Display for SortError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SortError::InvalidSortBy(field) => { + write!(f, "Sort by {} is invalid", field) + } + } + } +} + +impl Reject for SortError {} + +impl Reply for SortError { + fn into_response(self) -> Response { + let code = match self { + SortError::InvalidSortBy(_) => StatusCode::BAD_REQUEST, + }; + let message = self.to_string(); + + let json = warp::reply::json(&ErrorResponse { message }); + + warp::reply::with_status(json, code).into_response() + } +} diff --git a/src/issue/db.rs b/src/issue/db.rs index b178fc7..64bd6ba 100644 --- a/src/issue/db.rs +++ b/src/issue/db.rs @@ -11,7 +11,6 @@ use crate::db::{ }; use super::models::{Issue, IssueCreate}; -use super::utils::parse_github_issue_url; const TABLE: &str = "issues"; diff --git a/src/issue/handlers.rs b/src/issue/handlers.rs index dd066a5..641736f 100644 --- a/src/issue/handlers.rs +++ b/src/issue/handlers.rs @@ -9,7 +9,7 @@ use crate::{organization::db::DBOrganization, repository::db::DBRepository}; use super::{ db::DBIssue, errors::IssueError, - models::{IssueCreateRequest, IssueGetRequest, IssueResponse}, + models::{IssueCreateRequest, IssueResponse}, utils::parse_github_issue_url, }; @@ -20,7 +20,7 @@ pub async fn create_issue_handler( match db_access.get_issue_by_url(&body.url).await? { Some(u) => Err(warp::reject::custom(IssueError::IssueExists(u.id)))?, None => { - let info = parse_github_issue_url(&body.url)?; + _ = parse_github_issue_url(&body.url)?; // TODO: get or create both // db_access.create_organization(info.organization); // db_access.create_repository(info.repository); diff --git a/src/main.rs b/src/main.rs index a7f9bfa..2531e82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,5 @@ mod types; -use std::process; - -use db::utils::init_db; use warp::Filter; use crate::{ @@ -18,6 +15,7 @@ mod db; mod error; mod handlers; mod health; +mod http; mod issue; mod organization; mod repository; diff --git a/src/user/db.rs b/src/user/db.rs index 88a80d0..10493dd 100644 --- a/src/user/db.rs +++ b/src/user/db.rs @@ -2,15 +2,18 @@ use mobc::async_trait; use mobc_postgres::tokio_postgres::Row; use warp::reject; -use crate::db::{ - pool::DBAccess, - utils::{ - execute_query_with_timeout, query_one_timeout, query_opt_timeout, query_with_timeout, - DB_QUERY_TIMEOUT, +use crate::{ + db::{ + pool::DBAccess, + utils::{ + execute_query_with_timeout, query_one_timeout, query_opt_timeout, query_with_timeout, + DB_QUERY_TIMEOUT, + }, }, + http::GetPagination, }; -use super::models::{GetUsersFilters, NewUser, User, UsersFilters, UsersRelations}; +use super::models::{NewUser, User, UserSort, UsersRelations}; const TABLE: &str = "users"; @@ -29,7 +32,8 @@ pub trait DBUser: Send + Sync + Clone + 'static { async fn get_users( &self, relations: UsersRelations, - filters: UsersFilters, + pagination: GetPagination, + sort: UserSort, ) -> Result, reject::Rejection>; async fn create_user(&self, user: NewUser) -> Result; async fn delete_user(&self, id: i32) -> Result<(), reject::Rejection>; @@ -94,7 +98,8 @@ impl DBUser for DBAccess { async fn get_users( &self, relations: UsersRelations, - filters: UsersFilters, + pagination: GetPagination, + sort: UserSort, ) -> Result, reject::Rejection> { let mut query = format!("SELECT * FROM {} ", TABLE); if relations.maintainers { @@ -111,18 +116,14 @@ impl DBUser for DBAccess { query += "LEFT JOIN comments on comments.user_id = users.id "; query += "LEFT JOIN wishes on wishes.id = comments.wish_id "; } - // TODO: fix: ASC - query += "ORDER BY $1 ASC "; - query += "LIMIT $2 OFFSET $3"; + + query += &format!("ORDER BY {} {} ", sort.field, sort.order); // cannot use $1 or $2 here + + query += "LIMIT $1 OFFSET $2"; let rows = query_with_timeout( self, query.as_str(), - &[ - &filters.sort, - // &filters.ascending, - &filters.limit, - &filters.offset, - ], + &[&pagination.limit, &pagination.offset], DB_QUERY_TIMEOUT, ) .await?; diff --git a/src/user/handlers.rs b/src/user/handlers.rs index 1e3953c..7334cf0 100644 --- a/src/user/handlers.rs +++ b/src/user/handlers.rs @@ -4,10 +4,12 @@ use warp::{ reply::{json, Reply}, }; +use crate::http::{GetPagination, GetSort}; + use super::{ db::DBUser, errors::UserError, - models::{GetUserQuery, GetUsersFilters, NewUser, UserResponse, UsersFilters, UsersRelations}, + models::{GetUserQuery, NewUser, UserResponse, UserSort, UsersRelations}, }; pub async fn create_user_handler( @@ -62,7 +64,8 @@ pub async fn get_user_by_name_handler( pub async fn get_users_handler( db_access: impl DBUser, query: GetUserQuery, - filters: GetUsersFilters, + filters: GetPagination, + sort: GetSort, ) -> Result { let relations = UsersRelations { wishes: query.wishes.unwrap_or_default(), @@ -71,23 +74,20 @@ pub async fn get_users_handler( issues: query.issues.unwrap_or_default(), }; // TODO: validate filters (sort) - let default_filters = filters.apply_defaults(); - // TODO: create generic Pagination - // TODO: Improve asc/desc - let asc = { - if default_filters.ascending.unwrap() { - "ASC" - } else { - "DESC" - } - }; - let filters = UsersFilters { - limit: default_filters.limit.unwrap(), - offset: default_filters.offset.unwrap(), - sort: default_filters.sort.unwrap().to_string().into(), - ascending: asc.to_string(), + let pagination = filters.validate()?; + + let valid_fields = vec!["id", "username"]; //TODO improve with enum + let sort = sort.validate(valid_fields)?; + + let user_sort = if sort.sort_by.is_some() { + UserSort::new(&sort.sort_by.unwrap(), sort.descending.unwrap()) + } else { + UserSort::default() }; - let users = db_access.get_users(relations, filters).await?; + + let users = db_access + .get_users(relations, pagination, user_sort) + .await?; Ok(json::>( &users.into_iter().map(UserResponse::of).collect(), )) diff --git a/src/user/models.rs b/src/user/models.rs index 9078390..fa248db 100644 --- a/src/user/models.rs +++ b/src/user/models.rs @@ -1,5 +1,4 @@ -use std::default; - +use mobc_postgres::tokio_postgres::Error; use serde_derive::{Deserialize, Serialize}; #[derive(Deserialize)] @@ -28,33 +27,6 @@ impl UserResponse { } } -#[derive(Serialize, Deserialize, Default, Clone)] -pub enum UserSort { - #[default] - ById, - ByUsername, - ByCreatedAt, -} - -impl UserSort { - pub fn to_string(&self) -> &str { - match self { - UserSort::ById => "id", - UserSort::ByUsername => "username", - UserSort::ByCreatedAt => "created_at", - } - } -} -impl Default for GetUsersFilters { - fn default() -> Self { - GetUsersFilters { - limit: Some(1000), - offset: Some(0), - sort: Some("users.id".to_string()), - ascending: Some(true), - } - } -} #[derive(Default)] pub struct UsersRelations { pub wishes: bool, @@ -62,12 +34,6 @@ pub struct UsersRelations { pub maintainers: bool, pub issues: bool, } -pub struct UsersFilters { - pub limit: i64, - pub offset: i64, - pub sort: String, - pub ascending: String, -} // query args #[derive(Serialize, Deserialize, Default)] @@ -76,38 +42,49 @@ pub struct GetUserQuery { pub tips: Option, pub maintainers: Option, pub issues: Option, + // TODO: add filters + // pub is_maintainer: Option, + // pub has_tips: Option, + // pub has_issues: Option, + // pub has_wishes: Option, + // pub has_wishes: Option, } -#[derive(Serialize, Deserialize, Clone)] -pub struct GetUsersFilters { - pub limit: Option, - pub offset: Option, - pub sort: Option, - pub ascending: Option, +#[derive(Serialize, Deserialize)] +pub struct UserSort { + pub field: String, + pub order: String, + // TODO: add filters + // pub is_maintainer: Option, + // pub has_tips: Option, + // pub has_issues: Option, + // pub has_wishes: Option, + // pub has_wishes: Option, } +impl UserSort { + pub fn new(sort_by: &str, descending: bool) -> Self { + //TODO: validation may happen here, analyze -impl GetUsersFilters { - // A method to create an instance of GetUsersFilters with default values - pub fn new() -> Self { - GetUsersFilters::default() + Self { + field: sort_by.to_string(), + order: { + if descending { + "DESC".to_string() + } else { + "ASC".to_string() + } + }, + } } +} - // A method to set default values for individual fields if they are None - pub fn apply_defaults(&self) -> Self { - let mut filters = self.clone(); - let default = Self::default(); - if self.limit.is_none() { - filters.limit = default.limit; - } - if self.offset.is_none() { - filters.offset = default.offset; - } - if self.sort.is_none() { - filters.sort = default.sort; - } - if self.ascending.is_none() { - filters.ascending = default.ascending; +impl Default for UserSort { + fn default() -> Self { + UserSort { + field: "users.id".to_string(), + order: "ASC".to_string(), + // sort: Some("users.id".to_string()), + // ascending: Some(true), } - filters } } diff --git a/src/user/routes.rs b/src/user/routes.rs index 6944648..14b1ae3 100644 --- a/src/user/routes.rs +++ b/src/user/routes.rs @@ -1,14 +1,14 @@ use std::convert::Infallible; -use serde::{Deserialize, Serialize}; use warp::filters::BoxedFilter; use warp::{Filter, Reply}; use crate::auth::with_auth; +use crate::http::{GetPagination, GetSort}; use super::db::DBUser; use super::handlers; -use super::models::{GetUserQuery, GetUsersFilters}; +use super::models::GetUserQuery; fn with_db( db_pool: impl DBUser, @@ -25,7 +25,8 @@ pub fn routes(db_access: impl DBUser) -> BoxedFilter<(impl Reply,)> { .and(warp::get()) .and(with_db(db_access.clone())) .and(warp::query::()) - .and(warp::query::()) + .and(warp::query::()) + .and(warp::query::()) .and_then(handlers::get_users_handler); let get_user = user_id From ca6aee6391b2bdf63907d8e4827523dd86958370 Mon Sep 17 00:00:00 2001 From: Leandro Date: Mon, 15 Apr 2024 21:48:03 -0300 Subject: [PATCH 09/98] feat: improve sort and error handling --- src/handlers.rs | 8 ++++-- src/http.rs | 34 ++++++++++++------------ src/main.rs | 1 - src/types.rs | 3 --- src/user/handlers.rs | 13 ++++------ src/user/models.rs | 62 ++++++++++++++++++++++++++++++++++---------- 6 files changed, 75 insertions(+), 46 deletions(-) diff --git a/src/handlers.rs b/src/handlers.rs index 55839ab..861033d 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -4,8 +4,10 @@ use std::convert::Infallible; use warp::{hyper::StatusCode, Rejection, Reply}; use crate::{ - db::errors::DBError, error::AuthenticationError, organization::errors::OrganizationError, - user::errors::UserError, + db::errors::DBError, + error::AuthenticationError, + organization::errors::OrganizationError, + user::{errors::UserError, models::UserSortError}, }; #[derive(Serialize, Deserialize, PartialEq, Debug)] @@ -19,6 +21,8 @@ pub async fn error_handler(err: Rejection) -> std::result::Result() { Ok(e.clone().into_response()) + } else if let Some(e) = err.find::() { + Ok(e.clone().into_response()) } else if let Some(e) = err.find::() { Ok(e.clone().into_response()) } else if let Some(e) = err.find::() { diff --git a/src/http.rs b/src/http.rs index 51b119e..28e3628 100644 --- a/src/http.rs +++ b/src/http.rs @@ -19,11 +19,6 @@ pub struct GetPagination { } impl GetPagination { - // A method to create an instance of GetPagination with default values - pub fn new() -> Self { - GetPagination::default() - } - // A method to set default values for individual fields if they are None pub fn validate(&self) -> Result { let mut filters = self.clone(); @@ -109,27 +104,30 @@ pub struct GetSort { #[derive(Clone, Error, Debug, Deserialize, PartialEq)] pub enum SortError { - InvalidSortBy(String), + InvalidSortBy, } impl GetSort { - pub fn validate(&self, valid_fields: Vec<&str>) -> Result { + pub fn validate(&self) -> Result { match (self.sort_by.clone(), self.descending) { (None, None) => Ok(self.clone()), (None, Some(_)) => Ok(Self { sort_by: None, descending: None, }), - (Some(_), None) => Ok(Self { - sort_by: None, - descending: Some(false), - }), - (Some(sort_by), Some(_)) => { + (Some(sort_by), some_or_none) => { //TODO: improve with trim, remove unexpected chars, etc. - if valid_fields.iter().any(|&s| s == sort_by) { - Ok(self.clone()) + if sort_by.contains(",") { + Err(SortError::InvalidSortBy) } else { - Err(SortError::InvalidSortBy(sort_by)) + Ok(Self { + sort_by: Some(sort_by), + descending: if some_or_none.is_none() { + Some(false) + } else { + some_or_none + }, + }) } } } @@ -139,8 +137,8 @@ impl GetSort { impl fmt::Display for SortError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - SortError::InvalidSortBy(field) => { - write!(f, "Sort by {} is invalid", field) + SortError::InvalidSortBy => { + write!(f, "Sort by is invalid") } } } @@ -151,7 +149,7 @@ impl Reject for SortError {} impl Reply for SortError { fn into_response(self) -> Response { let code = match self { - SortError::InvalidSortBy(_) => StatusCode::BAD_REQUEST, + SortError::InvalidSortBy => StatusCode::BAD_REQUEST, }; let message = self.to_string(); diff --git a/src/main.rs b/src/main.rs index 2531e82..238dd45 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,7 +34,6 @@ async fn run() { http_server_host: host, http_server_port: port, database_url, - database_init_file, } = ApiConfig::new(); // init db diff --git a/src/types.rs b/src/types.rs index c1a4017..58a0029 100644 --- a/src/types.rs +++ b/src/types.rs @@ -10,8 +10,6 @@ pub struct ApiConfig { pub http_server_port: u16, /// Database URL. pub database_url: String, - /// Database init file. - pub database_init_file: String, } impl ApiConfig { @@ -25,7 +23,6 @@ impl ApiConfig { .parse() .expect("Invalid HTTP_SERVER_PORT"), database_url: env::var("DATABASE_URL").expect("DATABASE_URL must be set"), - database_init_file: env::var("DATABASE_INIT_FILE").unwrap_or_else(|_| "".to_owned()), } } } diff --git a/src/user/handlers.rs b/src/user/handlers.rs index 7334cf0..7f9a2db 100644 --- a/src/user/handlers.rs +++ b/src/user/handlers.rs @@ -75,15 +75,12 @@ pub async fn get_users_handler( }; // TODO: validate filters (sort) let pagination = filters.validate()?; - - let valid_fields = vec!["id", "username"]; //TODO improve with enum - let sort = sort.validate(valid_fields)?; - - let user_sort = if sort.sort_by.is_some() { - UserSort::new(&sort.sort_by.unwrap(), sort.descending.unwrap()) - } else { - UserSort::default() + let sort = sort.validate()?; + let user_sort = match (sort.sort_by, sort.descending) { + (Some(sort_by), Some(descending)) => UserSort::new(&sort_by, descending)?, + _ => UserSort::default(), }; + println!("here"); let users = db_access .get_users(relations, pagination, user_sort) diff --git a/src/user/models.rs b/src/user/models.rs index fa248db..6982b25 100644 --- a/src/user/models.rs +++ b/src/user/models.rs @@ -1,5 +1,14 @@ -use mobc_postgres::tokio_postgres::Error; +use std::fmt::{self}; + use serde_derive::{Deserialize, Serialize}; +use thiserror::Error; +use warp::{ + http::StatusCode, + reject::Reject, + reply::{Reply, Response}, +}; + +use crate::handlers::ErrorResponse; #[derive(Deserialize)] pub struct User { @@ -54,19 +63,15 @@ pub struct GetUserQuery { pub struct UserSort { pub field: String, pub order: String, - // TODO: add filters - // pub is_maintainer: Option, - // pub has_tips: Option, - // pub has_issues: Option, - // pub has_wishes: Option, - // pub has_wishes: Option, } impl UserSort { - pub fn new(sort_by: &str, descending: bool) -> Self { - //TODO: validation may happen here, analyze + pub fn new(field: &str, descending: bool) -> Result { + if field != "id" && field != "username" { + return Err(UserSortError::InvalidSortBy(field.to_owned())); + } - Self { - field: sort_by.to_string(), + Ok(Self { + field: format!("users.{field}"), order: { if descending { "DESC".to_string() @@ -74,7 +79,7 @@ impl UserSort { "ASC".to_string() } }, - } + }) } } @@ -83,8 +88,37 @@ impl Default for UserSort { UserSort { field: "users.id".to_string(), order: "ASC".to_string(), - // sort: Some("users.id".to_string()), - // ascending: Some(true), } } } + +#[derive(Clone, Error, Debug, Deserialize, PartialEq)] +pub enum UserSortError { + InvalidSortBy(String), +} + +impl fmt::Display for UserSortError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + UserSortError::InvalidSortBy(field) => { + write!(f, "Sort by {} is invalid", field) + } + } + } +} + +impl Reject for UserSortError {} + +impl Reply for UserSortError { + fn into_response(self) -> Response { + let status_code = match self { + UserSortError::InvalidSortBy(_) => StatusCode::BAD_REQUEST, + }; + let code = status_code; + let message = self.to_string(); + + let json = warp::reply::json(&ErrorResponse { message }); + + warp::reply::with_status(json, code).into_response() + } +} From 42953ac4f904888bf6c4224dcf21975b3bb78791 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sat, 20 Apr 2024 18:57:28 -0300 Subject: [PATCH 10/98] feat: add new endpoints --- Cargo.lock | 39 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/db/utils.rs | 16 ++++++++++++---- src/handlers.rs | 3 +++ src/http.rs | 8 ++++---- src/user/db.rs | 40 ++++++++++++++++++++++++++++++++++------ src/user/handlers.rs | 38 ++++++++++++++++++++++++++++++++------ src/user/models.rs | 8 +++++++- src/user/routes.rs | 23 ++++++++++++++++------- 9 files changed, 148 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9f1a1bd..4f53a00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -593,6 +602,7 @@ dependencies = [ "dotenv", "mobc", "mobc-postgres", + "regex", "serde", "serde_derive", "serde_json", @@ -977,6 +987,35 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + [[package]] name = "rustc-demangle" version = "0.1.23" diff --git a/Cargo.toml b/Cargo.toml index 9ff818d..878094a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,4 @@ serde_derive = "1.0.193" base64 = "0.22.0" url = "2.5.0" diesel = { version = "2.1.5", features = ["postgres"] } +regex = "1.10.4" diff --git a/src/db/utils.rs b/src/db/utils.rs index 8558117..8b812f0 100644 --- a/src/db/utils.rs +++ b/src/db/utils.rs @@ -1,8 +1,8 @@ use super::{errors::DBError, pool::DBAccessor}; use mobc_postgres::tokio_postgres::{types::ToSql, Row}; +use regex::Regex; use tokio::time::{timeout, Duration}; use warp::reject; - pub const DB_QUERY_TIMEOUT: Duration = Duration::from_secs(5); pub async fn query_with_timeout( @@ -32,7 +32,8 @@ pub async fn execute_query_with_timeout( timeout_duration: Duration, ) -> Result<(), reject::Rejection> { let db_conn = db_access.get_db_con().await.map_err(reject::custom)?; - + println!("{}", query); + println!("{:#?}", params); timeout(timeout_duration, db_conn.execute(query, params)) .await .map_err(|err| reject::custom(DBError::DBTimeout(err)))? @@ -47,7 +48,8 @@ pub async fn query_opt_timeout( timeout_duration: Duration, ) -> Result, reject::Rejection> { let db_conn = db_access.get_db_con().await.map_err(reject::custom)?; - + println!("{}", query); + println!("{:#?}", params); timeout(timeout_duration, db_conn.query_opt(query, params)) .await .map_err(|err| reject::custom(DBError::DBTimeout(err)))? @@ -61,9 +63,15 @@ pub async fn query_one_timeout( timeout_duration: Duration, ) -> Result { let db_conn = db_access.get_db_con().await.map_err(reject::custom)?; - + println!("{}", query); + println!("{:#?}", params); timeout(timeout_duration, db_conn.query_one(query, params)) .await .map_err(|err| reject::custom(DBError::DBTimeout(err)))? .map_err(|err| reject::custom(DBError::DBQuery(err))) } + +pub fn detect_sql_injection(input: &str) -> bool { + let sql_injection_pattern = Regex::new(r"(?i)(\b(?:select|insert|update|delete|drop|alter|create)\b.*\b(?:from|into|table|where)\b|\bunion\b.*\b(?:select|values)\b|\b(?:exec|execute)\b\s*\()").unwrap(); + sql_injection_pattern.is_match(input) +} diff --git a/src/handlers.rs b/src/handlers.rs index 861033d..016880d 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -7,6 +7,7 @@ use crate::{ db::errors::DBError, error::AuthenticationError, organization::errors::OrganizationError, + repository::errors::RepositoryError, user::{errors::UserError, models::UserSortError}, }; @@ -21,6 +22,8 @@ pub async fn error_handler(err: Rejection) -> std::result::Result() { Ok(e.clone().into_response()) + } else if let Some(e) = err.find::() { + Ok(e.clone().into_response()) } else if let Some(e) = err.find::() { Ok(e.clone().into_response()) } else if let Some(e) = err.find::() { diff --git a/src/http.rs b/src/http.rs index 28e3628..aaa1f73 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,5 +1,8 @@ use std::fmt; +use super::db::utils::detect_sql_injection; +use crate::handlers::ErrorResponse; +use serde::{Deserialize, Serialize}; use thiserror::Error; use warp::{ http::StatusCode, @@ -7,9 +10,6 @@ use warp::{ reply::{Reply, Response}, }; -use crate::handlers::ErrorResponse; -use serde::{Deserialize, Serialize}; - #[derive(Serialize, Deserialize, Clone)] pub struct GetPagination { pub limit: Option, @@ -117,7 +117,7 @@ impl GetSort { }), (Some(sort_by), some_or_none) => { //TODO: improve with trim, remove unexpected chars, etc. - if sort_by.contains(",") { + if detect_sql_injection(&sort_by) { Err(SortError::InvalidSortBy) } else { Ok(Self { diff --git a/src/user/db.rs b/src/user/db.rs index 10493dd..b164a2d 100644 --- a/src/user/db.rs +++ b/src/user/db.rs @@ -1,7 +1,3 @@ -use mobc::async_trait; -use mobc_postgres::tokio_postgres::Row; -use warp::reject; - use crate::{ db::{ pool::DBAccess, @@ -12,8 +8,11 @@ use crate::{ }, http::GetPagination, }; +use mobc::async_trait; +use mobc_postgres::tokio_postgres::Row; +use warp::reject; -use super::models::{NewUser, User, UserSort, UsersRelations}; +use super::models::{NewUser, PatchUser, User, UserSort, UsersRelations}; const TABLE: &str = "users"; @@ -36,6 +35,11 @@ pub trait DBUser: Send + Sync + Clone + 'static { sort: UserSort, ) -> Result, reject::Rejection>; async fn create_user(&self, user: NewUser) -> Result; + async fn update_user_maintainers( + &self, + id: i32, + user: PatchUser, + ) -> Result; async fn delete_user(&self, id: i32) -> Result<(), reject::Rejection>; } @@ -117,7 +121,7 @@ impl DBUser for DBAccess { query += "LEFT JOIN wishes on wishes.id = comments.wish_id "; } - query += &format!("ORDER BY {} {} ", sort.field, sort.order); // cannot use $1 or $2 here + query += &format!("ORDER BY {} {}", sort.field, sort.order); // cannot use $1 or $2 query += "LIMIT $1 OFFSET $2"; let rows = query_with_timeout( @@ -133,6 +137,30 @@ impl DBUser for DBAccess { async fn create_user(&self, user: NewUser) -> Result { let query = format!("INSERT INTO {} (username) VALUES ($1) RETURNING *", TABLE); let row = query_one_timeout(self, &query, &[&user.username], DB_QUERY_TIMEOUT).await?; + let new_user = row_to_user(&row); + + if let Some(repositories) = user.repositories { + for repo_id in repositories { + let query = + format!("INSERT INTO maintainers (user_id, repository_id) VALUES ($1, $2)"); + query_one_timeout(self, &query, &[&new_user.id, &repo_id], DB_QUERY_TIMEOUT) + .await?; + } + } + Ok(new_user) + } + async fn update_user_maintainers( + &self, + id: i32, + user: PatchUser, + ) -> Result { + let query = format!("DELETE maintainers WHERE user_ID = $1"); + let row = query_one_timeout(self, &query, &[&id], DB_QUERY_TIMEOUT).await?; + for repo_id in user.repositories { + let query = format!("INSERT INTO maintainers (user_id, repository_id) VALUES ($1, $2)"); + query_one_timeout(self, &query, &[&id, &repo_id], DB_QUERY_TIMEOUT).await?; + } + // TODO: make a db tx Ok(row_to_user(&row)) } diff --git a/src/user/handlers.rs b/src/user/handlers.rs index 7f9a2db..f9bd0cb 100644 --- a/src/user/handlers.rs +++ b/src/user/handlers.rs @@ -4,24 +4,52 @@ use warp::{ reply::{json, Reply}, }; -use crate::http::{GetPagination, GetSort}; +use crate::{ + http::{GetPagination, GetSort}, + repository::{db::DBRepository, errors::RepositoryError}, +}; use super::{ db::DBUser, errors::UserError, - models::{GetUserQuery, NewUser, UserResponse, UserSort, UsersRelations}, + models::{GetUserQuery, NewUser, PatchUser, UserResponse, UserSort, UsersRelations}, }; pub async fn create_user_handler( body: NewUser, - db_access: impl DBUser, + db_access: impl DBUser + DBRepository, ) -> Result { match db_access .get_user_by_username(&body.username, UsersRelations::default()) .await? { Some(u) => Err(warp::reject::custom(UserError::UserExists(u.id)))?, - None => Ok(json(&UserResponse::of(db_access.create_user(body).await?))), + None => { + if let Some(repositories) = body.repositories.clone() { + for repo_id in repositories { + let result = db_access.get_repository(repo_id).await?; + if result.is_none() { + // TODO: improve + return Err(warp::reject::custom(RepositoryError::RepositoryNotFound( + repo_id, + ))); + } + } + } + Ok(json(&UserResponse::of(db_access.create_user(body).await?))) + } + } +} +pub async fn patch_user_handler( + id: i32, + body: PatchUser, + db_access: impl DBUser, +) -> Result { + match db_access.get_user(id, UsersRelations::default()).await? { + Some(u) => Err(warp::reject::custom(UserError::UserExists(u.id)))?, + None => Ok(json(&UserResponse::of( + db_access.update_user_maintainers(id, body).await?, + ))), } } @@ -73,14 +101,12 @@ pub async fn get_users_handler( maintainers: query.maintainers.unwrap_or_default(), issues: query.issues.unwrap_or_default(), }; - // TODO: validate filters (sort) let pagination = filters.validate()?; let sort = sort.validate()?; let user_sort = match (sort.sort_by, sort.descending) { (Some(sort_by), Some(descending)) => UserSort::new(&sort_by, descending)?, _ => UserSort::default(), }; - println!("here"); let users = db_access .get_users(relations, pagination, user_sort) diff --git a/src/user/models.rs b/src/user/models.rs index 6982b25..e24d7ea 100644 --- a/src/user/models.rs +++ b/src/user/models.rs @@ -14,11 +14,17 @@ use crate::handlers::ErrorResponse; pub struct User { pub id: i32, pub username: String, + // pub maintainers: Option>, } #[derive(Serialize, Deserialize)] pub struct NewUser { pub username: String, + pub repositories: Option>, +} +#[derive(Serialize, Deserialize)] +pub struct PatchUser { + pub repositories: Vec, } #[derive(Serialize, Deserialize, PartialEq, Debug)] @@ -86,7 +92,7 @@ impl UserSort { impl Default for UserSort { fn default() -> Self { UserSort { - field: "users.id".to_string(), + field: "id".to_string(), order: "ASC".to_string(), } } diff --git a/src/user/routes.rs b/src/user/routes.rs index 14b1ae3..10a913c 100644 --- a/src/user/routes.rs +++ b/src/user/routes.rs @@ -5,21 +5,23 @@ use warp::{Filter, Reply}; use crate::auth::with_auth; use crate::http::{GetPagination, GetSort}; +use crate::repository::db::DBRepository; use super::db::DBUser; use super::handlers; use super::models::GetUserQuery; fn with_db( - db_pool: impl DBUser, -) -> impl Filter + Clone { + db_pool: impl DBUser + DBRepository, +) -> impl Filter + Clone { warp::any().map(move || db_pool.clone()) } -pub fn routes(db_access: impl DBUser) -> BoxedFilter<(impl Reply,)> { +pub fn routes(db_access: impl DBUser + DBRepository) -> BoxedFilter<(impl Reply,)> { let user = warp::path!("users"); let user_id = warp::path!("users" / i32); - let user_name = warp::path!("users" / String); + let user_name = warp::path!("users" / "username" / String); + let user_maintainer = warp::path!("users" / "maintainers" / i32); let get_users = user .and(warp::get()) @@ -40,14 +42,20 @@ pub fn routes(db_access: impl DBUser) -> BoxedFilter<(impl Reply,)> { .and(with_db(db_access.clone())) .and(warp::query::()) .and_then(handlers::get_user_by_name_handler); - // TODO: add maintainers + let create_user = user .and(with_auth()) .and(warp::post()) .and(warp::body::json()) .and(with_db(db_access.clone())) .and_then(handlers::create_user_handler); - // TODO: add PATCH maintainers + let patch_user = user_maintainer + .and(with_auth()) + .and(warp::patch()) + .and(warp::body::json()) + .and(with_db(db_access.clone())) + .and_then(handlers::patch_user_handler); + let delete_user = user_id .and(with_auth()) .and(warp::delete()) @@ -58,7 +66,8 @@ pub fn routes(db_access: impl DBUser) -> BoxedFilter<(impl Reply,)> { .or(create_user) .or(get_user) .or(delete_user) - .or(get_user_by_name); + .or(get_user_by_name) + .or(patch_user); route.boxed() } From 26fc5f041595421930761b958cdd6cca49019709 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 21 Apr 2024 21:51:31 -0300 Subject: [PATCH 11/98] feat: improve tests and minor fixes --- src/auth.rs | 23 +- src/{error.rs => auth_error.rs} | 18 +- src/db/utils.rs | 15 + src/{handlers.rs => error_handler.rs} | 5 +- src/issue/errors.rs | 2 +- src/main.rs | 8 +- src/organization/errors.rs | 2 +- src/{http.rs => pagination.rs} | 3 +- src/repository/errors.rs | 2 +- src/tests/mod.rs | 2 + src/tests/repositories.rs | 83 +++++ src/tests/users.rs | 419 ++++++++++++++++++++++++++ src/user/db.rs | 9 +- src/user/errors.rs | 36 ++- src/user/handlers.rs | 52 ++-- src/user/models.rs | 18 +- src/user/routes.rs | 4 +- 17 files changed, 619 insertions(+), 82 deletions(-) rename src/{error.rs => auth_error.rs} (69%) rename src/{handlers.rs => error_handler.rs} (94%) rename src/{http.rs => pagination.rs} (97%) create mode 100644 src/tests/repositories.rs create mode 100644 src/tests/users.rs diff --git a/src/auth.rs b/src/auth.rs index ef23b0d..ac2bf4a 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,4 +1,4 @@ -use crate::error::AuthenticationError; +use crate::auth_error::AuthenticationError; use std::env; use warp::{ http::header::{HeaderMap, HeaderValue, AUTHORIZATION}, @@ -14,24 +14,21 @@ async fn authorize(headers: HeaderMap) -> Result<(), Rejection> { match token_from_header(&headers) { Ok(token) => { let credentials = base64::decode(token) - .map_err(|_| reject::custom(AuthenticationError::WrongCredentialsError))?; + .map_err(|_| reject::custom(AuthenticationError::WrongCredentials))?; let credentials_str = String::from_utf8(credentials) - .map_err(|_| reject::custom(AuthenticationError::WrongCredentialsError))?; + .map_err(|_| reject::custom(AuthenticationError::WrongCredentials))?; let credentials: Vec<&str> = credentials_str.split(':').collect(); if credentials.len() == 2 { - let expected_username = env::var("USERNAME") - .map_err(|_| reject::custom(AuthenticationError::WrongCredentialsError))?; - let expected_password = env::var("PASSWORD") - .map_err(|_| reject::custom(AuthenticationError::WrongCredentialsError))?; - + let expected_username = env::var("USERNAME").unwrap_or("test".to_string()); + let expected_password = env::var("PASSWORD").unwrap_or("test".to_string()); if credentials[0] == expected_username && credentials[1] == expected_password { Ok(()) } else { - Err(reject::custom(AuthenticationError::WrongCredentialsError)) + Err(reject::custom(AuthenticationError::WrongCredentials)) } } else { - Err(reject::custom(AuthenticationError::WrongCredentialsError)) + Err(reject::custom(AuthenticationError::WrongCredentials)) } } Err(e) => Err(reject::custom(e)), @@ -41,12 +38,12 @@ async fn authorize(headers: HeaderMap) -> Result<(), Rejection> { fn token_from_header(headers: &HeaderMap) -> Result { let header = headers .get(AUTHORIZATION) - .ok_or(AuthenticationError::NoAuthHeaderError)?; + .ok_or(AuthenticationError::NoAuthHeader)?; let auth_header = std::str::from_utf8(header.as_bytes()) - .map_err(|_| AuthenticationError::InvalidAuthHeaderError)?; + .map_err(|_| AuthenticationError::InvalidAuthHeader)?; if !auth_header.starts_with(BASIC) { - return Err(AuthenticationError::InvalidAuthHeaderError); + return Err(AuthenticationError::InvalidAuthHeader); } Ok(auth_header.trim_start_matches(BASIC).to_owned()) } diff --git a/src/error.rs b/src/auth_error.rs similarity index 69% rename from src/error.rs rename to src/auth_error.rs index e89d7cf..01f0a8e 100644 --- a/src/error.rs +++ b/src/auth_error.rs @@ -3,27 +3,27 @@ use std::fmt; use thiserror::Error; use warp::{http::StatusCode, reject::Reject, reply::Response, Reply}; -use crate::handlers::ErrorResponse; +use crate::error_handler::ErrorResponse; #[derive(Clone, Error, Debug, Deserialize, PartialEq)] pub enum AuthenticationError { - WrongCredentialsError, - BasicTokenError, - NoAuthHeaderError, - InvalidAuthHeaderError, + WrongCredentials, + BasicToken, + NoAuthHeader, + InvalidAuthHeader, } impl fmt::Display for AuthenticationError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - AuthenticationError::WrongCredentialsError => { + AuthenticationError::WrongCredentials => { write!(f, "Wrong credentials") } - AuthenticationError::BasicTokenError => { + AuthenticationError::BasicToken => { write!(f, "Basic Token Error") } - AuthenticationError::NoAuthHeaderError => write!(f, "No Authorization Header"), - AuthenticationError::InvalidAuthHeaderError => { + AuthenticationError::NoAuthHeader => write!(f, "No Authorization Header"), + AuthenticationError::InvalidAuthHeader => { write!(f, "Invalid Authorization Header") } } diff --git a/src/db/utils.rs b/src/db/utils.rs index 8b812f0..407c45e 100644 --- a/src/db/utils.rs +++ b/src/db/utils.rs @@ -3,7 +3,10 @@ use mobc_postgres::tokio_postgres::{types::ToSql, Row}; use regex::Regex; use tokio::time::{timeout, Duration}; use warp::reject; + pub const DB_QUERY_TIMEOUT: Duration = Duration::from_secs(5); +pub const ASC: &str = "ASC"; +pub const DESC: &str = "DESC"; pub async fn query_with_timeout( db_access: &impl DBAccessor, @@ -75,3 +78,15 @@ pub fn detect_sql_injection(input: &str) -> bool { let sql_injection_pattern = Regex::new(r"(?i)(\b(?:select|insert|update|delete|drop|alter|create)\b.*\b(?:from|into|table|where)\b|\bunion\b.*\b(?:select|values)\b|\b(?:exec|execute)\b\s*\()").unwrap(); sql_injection_pattern.is_match(input) } + +pub fn sort_direction(descending: bool) -> String { + if descending { + DESC.to_string() + } else { + ASC.to_string() + } +} + +pub fn defaul_sort_direction() -> String { + ASC.to_string() +} diff --git a/src/handlers.rs b/src/error_handler.rs similarity index 94% rename from src/handlers.rs rename to src/error_handler.rs index 016880d..55511e3 100644 --- a/src/handlers.rs +++ b/src/error_handler.rs @@ -4,9 +4,10 @@ use std::convert::Infallible; use warp::{hyper::StatusCode, Rejection, Reply}; use crate::{ + auth_error::AuthenticationError, db::errors::DBError, - error::AuthenticationError, organization::errors::OrganizationError, + pagination::PaginationError, repository::errors::RepositoryError, user::{errors::UserError, models::UserSortError}, }; @@ -26,6 +27,8 @@ pub async fn error_handler(err: Rejection) -> std::result::Result() { Ok(e.clone().into_response()) + } else if let Some(e) = err.find::() { + Ok(e.clone().into_response()) } else if let Some(e) = err.find::() { Ok(e.clone().into_response()) } else if let Some(e) = err.find::() { diff --git a/src/issue/errors.rs b/src/issue/errors.rs index f36dedc..92591f8 100644 --- a/src/issue/errors.rs +++ b/src/issue/errors.rs @@ -8,7 +8,7 @@ use warp::{ reply::{Reply, Response}, }; -use crate::handlers::ErrorResponse; +use crate::error_handler::ErrorResponse; #[derive(Clone, Error, Debug, Deserialize, PartialEq)] pub enum IssueError { diff --git a/src/main.rs b/src/main.rs index 238dd45..a845d73 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,13 +11,13 @@ use crate::{ }; mod auth; +mod auth_error; mod db; -mod error; -mod handlers; +mod error_handler; mod health; -mod http; mod issue; mod organization; +mod pagination; mod repository; mod user; @@ -47,7 +47,7 @@ async fn run() { let organizations_route = organization::routes::routes(db.clone()); let repositories_route = repository::routes::routes(db); //TODO: add issue route - let error_handler = handlers::error_handler; + let error_handler = error_handler::error_handler; // string all the routes together let routes = health_route diff --git a/src/organization/errors.rs b/src/organization/errors.rs index fe4c209..5c3f04b 100644 --- a/src/organization/errors.rs +++ b/src/organization/errors.rs @@ -8,7 +8,7 @@ use warp::{ reply::{Reply, Response}, }; -use crate::handlers::ErrorResponse; +use crate::error_handler::ErrorResponse; #[derive(Clone, Error, Debug, Deserialize, PartialEq)] pub enum OrganizationError { diff --git a/src/http.rs b/src/pagination.rs similarity index 97% rename from src/http.rs rename to src/pagination.rs index aaa1f73..dcd6150 100644 --- a/src/http.rs +++ b/src/pagination.rs @@ -1,7 +1,7 @@ use std::fmt; use super::db::utils::detect_sql_injection; -use crate::handlers::ErrorResponse; +use crate::error_handler::ErrorResponse; use serde::{Deserialize, Serialize}; use thiserror::Error; use warp::{ @@ -116,7 +116,6 @@ impl GetSort { descending: None, }), (Some(sort_by), some_or_none) => { - //TODO: improve with trim, remove unexpected chars, etc. if detect_sql_injection(&sort_by) { Err(SortError::InvalidSortBy) } else { diff --git a/src/repository/errors.rs b/src/repository/errors.rs index 20f5d98..1212c3e 100644 --- a/src/repository/errors.rs +++ b/src/repository/errors.rs @@ -8,7 +8,7 @@ use warp::{ reply::{Reply, Response}, }; -use crate::handlers::ErrorResponse; +use crate::error_handler::ErrorResponse; #[derive(Clone, Error, Debug, Deserialize, PartialEq)] pub enum RepositoryError { diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 99d31de..33e6c7d 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,2 +1,4 @@ pub mod contribution; pub mod health; +pub mod repositories; +pub mod users; diff --git a/src/tests/repositories.rs b/src/tests/repositories.rs new file mode 100644 index 0000000..1f8997c --- /dev/null +++ b/src/tests/repositories.rs @@ -0,0 +1,83 @@ +#[cfg(test)] +pub mod tests { + use crate::repository::{ + db::DBRepository, + models::{Repository, RepositoryRequest}, + }; + use mobc::async_trait; + use warp::reject; + + #[derive(Clone)] + pub struct RepositoriesDBMockValues {} + #[derive(Clone)] + pub struct RepositoriesDBMockEmpty {} + + #[async_trait] + impl DBRepository for RepositoriesDBMockValues { + async fn get_repository(&self, id: i32) -> Result, reject::Rejection> { + Ok(Some(Repository { + id, + name: "repo".to_owned(), + organization_id: 1, + })) + } + async fn get_repository_by_name( + &self, + name: &str, + ) -> Result, reject::Rejection> { + Ok(Some(Repository { + id: 1, + name: name.to_owned(), + organization_id: 1, + })) + } + async fn get_repositories(&self) -> Result, reject::Rejection> { + Ok(vec![Repository { + name: "repo".to_owned(), + organization_id: 1, + id: 1, + }]) + } + async fn create_repository( + &self, + repository: RepositoryRequest, + ) -> Result { + Ok(Repository { + name: repository.name.to_string(), + id: 1, + organization_id: repository.organization_id, + }) + } + async fn delete_repository(&self, _: i32) -> Result<(), reject::Rejection> { + Ok(()) + } + } + #[async_trait] + impl DBRepository for RepositoriesDBMockEmpty { + async fn get_repository(&self, _: i32) -> Result, reject::Rejection> { + Ok(None) + } + async fn get_repository_by_name( + &self, + _: &str, + ) -> Result, reject::Rejection> { + Ok(None) + } + async fn get_repositories(&self) -> Result, reject::Rejection> { + Ok(vec![]) + } + async fn create_repository( + &self, + repository: RepositoryRequest, + ) -> Result { + Ok(Repository { + name: repository.name.to_string(), + id: 1, + organization_id: repository.organization_id, + }) + } + async fn delete_repository(&self, _: i32) -> Result<(), reject::Rejection> { + Ok(()) + } + } +} diff --git a/src/tests/users.rs b/src/tests/users.rs new file mode 100644 index 0000000..e4a595c --- /dev/null +++ b/src/tests/users.rs @@ -0,0 +1,419 @@ +#[cfg(test)] +mod tests { + use crate::{ + error_handler::{self, ErrorResponse}, + pagination::GetPagination, + repository::{ + db::DBRepository, + models::{Repository, RepositoryRequest}, + }, + user::{ + self, + db::DBUser, + models::{NewUser, PatchUser, User, UserResponse, UserSort, UsersRelations}, + routes::routes, + }, + }; + use diesel::expression::is_aggregate::No; + use mobc::async_trait; + use warp::{reject, test::request, Filter}; + + #[derive(Clone)] + pub struct UsersDBMockValues {} + #[derive(Clone)] + pub struct UsersDBMockEmpty {} + + #[derive(Clone)] + pub struct UsersAndRepositoriesDBMockValues {} + + #[async_trait] + impl DBUser for UsersDBMockValues { + async fn get_user( + &self, + id: i32, + relations: UsersRelations, + ) -> Result, reject::Rejection> { + if id == 1 { + Ok(Some(User { + id: 1, + username: "username".to_string(), + })) + } else { + Ok(None) + } + } + async fn get_user_by_username( + &self, + username: &str, + relations: UsersRelations, + ) -> Result, reject::Rejection> { + Ok(Some(User { + id: 1, + username: username.to_string(), + })) + } + async fn get_users( + &self, + relations: UsersRelations, + pagination: GetPagination, + sort: UserSort, + ) -> Result, reject::Rejection> { + Ok(vec![User { + id: 1, + username: "username".to_string(), + }]) + } + async fn create_user(&self, user: NewUser) -> Result { + Ok(User { + id: 1, + username: "username".to_string(), + }) + } + async fn update_user_maintainers( + &self, + id: i32, + user: PatchUser, + ) -> Result { + Ok(User { + id: 1, + username: "username".to_string(), + }) + } + async fn delete_user(&self, _: i32) -> Result<(), reject::Rejection> { + Ok(()) + } + } + #[async_trait] + impl DBUser for UsersDBMockEmpty { + async fn get_user( + &self, + id: i32, + _: UsersRelations, + ) -> Result, reject::Rejection> { + if id == 1 { + Ok(Some(User { + id: 1, + username: "username".to_string(), + })) + } else { + Ok(None) + } + } + async fn get_users( + &self, + _: UsersRelations, + _: GetPagination, + _: UserSort, + ) -> Result, reject::Rejection> { + Ok(vec![]) + } + async fn get_user_by_username( + &self, + username: &str, + _: UsersRelations, + ) -> Result, reject::Rejection> { + if username == "username" { + Ok(Some(User { + id: 1, + username: username.to_string(), + })) + } else { + Ok(None) + } + } + async fn create_user(&self, user: NewUser) -> Result { + Ok(User { + id: 1, + username: user.username, + }) + } + async fn delete_user(&self, _: i32) -> Result<(), reject::Rejection> { + Ok(()) + } + async fn update_user_maintainers( + &self, + _: i32, + _: PatchUser, + ) -> Result { + Ok(User { + id: 1, + username: "username".to_string(), + }) + } + } + + #[async_trait] + impl DBRepository for UsersDBMockEmpty { + async fn get_repository(&self, _: i32) -> Result, reject::Rejection> { + Ok(None) + } + async fn get_repository_by_name( + &self, + _: &str, + ) -> Result, reject::Rejection> { + Ok(None) + } + async fn get_repositories(&self) -> Result, reject::Rejection> { + Ok(vec![]) + } + async fn create_repository( + &self, + _: RepositoryRequest, + ) -> Result { + Ok(Repository { + id: 1, + name: "repo".to_owned(), + organization_id: 1, + }) + } + async fn delete_repository(&self, _: i32) -> Result<(), reject::Rejection> { + Ok(()) + } + } + #[tokio::test] + async fn test_get_user_by_id_not_found() { + let id = 2; + let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let resp = request().path(&format!("/users/{id}")).reply(&r).await; + assert_eq!(resp.status(), 404); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); + + assert_eq!( + response, + ErrorResponse { + message: format!("User #{id} not found",), + } + ) + } + #[tokio::test] + async fn test_get_user_by_id_exists() { + let id = 1; + let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let resp = request().path(&format!("/users/{id}")).reply(&r).await; + assert_eq!(resp.status(), 200); + let body = resp.into_body(); + let response: UserResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!( + response, + UserResponse { + id, + username: "username".to_string(), + } + ) + } + #[tokio::test] + async fn test_get_user_by_name_not_found() { + let name = "not_found"; + let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let resp = request() + .path(&format!("/users/username/{name}")) + .reply(&r) + .await; + assert_eq!(resp.status(), 404); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!( + response, + ErrorResponse { + message: format!("User {name} not found",), + } + ) + } + #[tokio::test] + async fn test_get_user_by_name_exists() { + let name = "username"; + let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let resp = request() + .path(&format!("/users/username/{name}")) + .reply(&r) + .await; + assert_eq!(resp.status(), 200); + let body = resp.into_body(); + let expected_response = UserResponse { + id: 1, + username: name.to_string(), + }; + let response: UserResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!(response, expected_response) + } + #[tokio::test] + async fn test_get_users() { + let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let resp = request().path(&format!("/users")).reply(&r).await; + assert_eq!(resp.status(), 200); + let body = resp.into_body(); + let response: Vec = serde_json::from_slice(&body).unwrap(); + assert_eq!(response, vec![]); + } + #[tokio::test] + async fn test_get_users_valid_query_params() { + let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let resp = request().path(&format!("/users?wishes=false&tips=false&maintainers=false&issues=false&limit=1&offset=10&sort_by=id&descending=false")).reply(&r).await; + assert_eq!(resp.status(), 200); + let body = resp.into_body(); + let response: Vec = serde_json::from_slice(&body).unwrap(); + assert_eq!(response, vec![]); + } + #[tokio::test] + async fn test_get_users_invalid_query_params() { + let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let resp = request() + .path(&format!("/users?wishes=fal1se")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); // TODO: check why it's a 401 + let resp = request() + .path(&format!("/users?maintainers=fal1se")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); // TODO: check why it's a 401 + let resp = request() + .path(&format!("/users?issues=123")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); // TODO: check why it's a 401 + let resp = request().path(&format!("/users?tips=123")).reply(&r).await; + assert_eq!(resp.status(), 401); + let resp = request().path(&format!("/users?limit=asd")).reply(&r).await; + assert_eq!(resp.status(), 401); + let resp = request() + .path(&format!("/users?offset=asd")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); + let resp = request() + .path(&format!("/users?sort_by=invalid")) + .reply(&r) + .await; + assert_eq!(resp.status(), 400); + let resp = request() + .path(&format!("/users?descending=sadf")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); + } + + #[tokio::test] + async fn test_create_user_ok() { + let id = 1; + let username = "new".to_string(); + let new_user: Vec = serde_json::to_vec(&NewUser { + username: username.clone(), + repositories: None, + }) + .unwrap(); + let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let resp = request() + .body(new_user) + .path(&"/users") + .header("Authorization", "Basic dGVzdDp0ZXN0") // test:test + .method("POST") + .reply(&r) + .await; + assert_eq!(resp.status(), 201); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let expected_response = UserResponse { id, username }; + let response: UserResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!(response, expected_response) + } + + #[tokio::test] + async fn test_patch_user_ok() { + let new_user: Vec = serde_json::to_vec(&PatchUser { + repositories: vec![1], + }) + .unwrap(); + let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let resp = request() + .body(new_user) + .path(&"/users/1/maintainers") + .header("Authorization", "Basic dGVzdDp0ZXN0") // test:test + .method("PATCH") + .reply(&r) + .await; + assert_eq!(resp.status(), 422); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!( + response, + ErrorResponse { + message: format!("User #1 cannot be updated: repository 1 does not exist",), + } + ) + } + #[tokio::test] + async fn test_create_user_already_exists() { + let username = "username".to_string(); + let new_user: Vec = serde_json::to_vec(&NewUser { + username: username.clone(), + repositories: None, + }) + .unwrap(); + let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let resp = request() + .body(new_user) + .path(&"/users") + .header("Authorization", "Basic dGVzdDp0ZXN0") // test:test + .method("POST") + .reply(&r) + .await; + assert_eq!(resp.status(), 400); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!( + response, + ErrorResponse { + message: format!("User #1 already exists",), + } + ) + } + + #[tokio::test] + async fn test_delete_user() { + let id = 1; + let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let resp = request() + .path(&format!("/users/{id}")) + .method("DELETE") + .header("Authorization", "Basic dGVzdDp0ZXN0") // test:test + .reply(&r) + .await; + assert_eq!(resp.status(), 204); + let body = resp.into_body(); + assert!(body.is_empty()); + } + + #[tokio::test] + async fn test_delete_user_does_not_exist_mock_db() { + let id = 4; + + let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let resp = request() + .path(&format!("/users/4")) + .method("DELETE") + .header("Authorization", "Basic dGVzdDp0ZXN0") // test:test + .reply(&r) + .await; + + assert_eq!(resp.status(), 404); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let expected_response = ErrorResponse { + message: format!("User #{id} not found"), + }; + let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!(response, expected_response) + } +} diff --git a/src/user/db.rs b/src/user/db.rs index b164a2d..760b06f 100644 --- a/src/user/db.rs +++ b/src/user/db.rs @@ -6,7 +6,7 @@ use crate::{ DB_QUERY_TIMEOUT, }, }, - http::GetPagination, + pagination::GetPagination, }; use mobc::async_trait; use mobc_postgres::tokio_postgres::Row; @@ -142,7 +142,7 @@ impl DBUser for DBAccess { if let Some(repositories) = user.repositories { for repo_id in repositories { let query = - format!("INSERT INTO maintainers (user_id, repository_id) VALUES ($1, $2)"); + "INSERT INTO maintainers (user_id, repository_id) VALUES ($1, $2)".to_string(); query_one_timeout(self, &query, &[&new_user.id, &repo_id], DB_QUERY_TIMEOUT) .await?; } @@ -154,10 +154,11 @@ impl DBUser for DBAccess { id: i32, user: PatchUser, ) -> Result { - let query = format!("DELETE maintainers WHERE user_ID = $1"); + let query = "DELETE maintainers WHERE user_ID = $1".to_string(); let row = query_one_timeout(self, &query, &[&id], DB_QUERY_TIMEOUT).await?; for repo_id in user.repositories { - let query = format!("INSERT INTO maintainers (user_id, repository_id) VALUES ($1, $2)"); + let query = + "INSERT INTO maintainers (user_id, repository_id) VALUES ($1, $2)".to_string(); query_one_timeout(self, &query, &[&id, &repo_id], DB_QUERY_TIMEOUT).await?; } // TODO: make a db tx diff --git a/src/user/errors.rs b/src/user/errors.rs index 658a5c0..f468316 100644 --- a/src/user/errors.rs +++ b/src/user/errors.rs @@ -8,26 +8,34 @@ use warp::{ reply::{Reply, Response}, }; -use crate::handlers::ErrorResponse; +use crate::error_handler::ErrorResponse; #[derive(Clone, Error, Debug, Deserialize, PartialEq)] pub enum UserError { - UserExists(i32), - UserNotFound(i32), - UserNotFoundByName(String), + AlreadyExists(i32), + NotFound(i32), + NotFoundByName(String), + CannotBeCreated(String), + CannotBeUpdated(i32, String), } impl fmt::Display for UserError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - UserError::UserExists(id) => { - write!(f, "User #{} already exists", id) + UserError::AlreadyExists(id) => { + write!(f, "User #{id} already exists") } - UserError::UserNotFound(id) => { - write!(f, "User #{} not found", id) + UserError::NotFound(id) => { + write!(f, "User #{id} not found") } - UserError::UserNotFoundByName(name) => { - write!(f, "User {} not found", name) + UserError::NotFoundByName(name) => { + write!(f, "User {name} not found") + } + UserError::CannotBeCreated(error) => { + write!(f, "User cannot be created: {error}") + } + UserError::CannotBeUpdated(id, error) => { + write!(f, "User #{id} cannot be updated: {error}") } } } @@ -38,9 +46,11 @@ impl Reject for UserError {} impl Reply for UserError { fn into_response(self) -> Response { let code = match self { - UserError::UserExists(_) => StatusCode::BAD_REQUEST, - UserError::UserNotFound(_) => StatusCode::NOT_FOUND, - UserError::UserNotFoundByName(_) => StatusCode::NOT_FOUND, + UserError::AlreadyExists(_) => StatusCode::BAD_REQUEST, + UserError::NotFound(_) => StatusCode::NOT_FOUND, + UserError::NotFoundByName(_) => StatusCode::NOT_FOUND, + UserError::CannotBeCreated(_) => StatusCode::UNPROCESSABLE_ENTITY, + UserError::CannotBeUpdated(_, _) => StatusCode::UNPROCESSABLE_ENTITY, }; let message = self.to_string(); diff --git a/src/user/handlers.rs b/src/user/handlers.rs index f9bd0cb..0c9b8ff 100644 --- a/src/user/handlers.rs +++ b/src/user/handlers.rs @@ -1,12 +1,12 @@ use warp::{ http::StatusCode, reject::Rejection, - reply::{json, Reply}, + reply::{json, with_status, Reply}, }; use crate::{ - http::{GetPagination, GetSort}, - repository::{db::DBRepository, errors::RepositoryError}, + pagination::{GetPagination, GetSort}, + repository::db::DBRepository, }; use super::{ @@ -23,33 +23,45 @@ pub async fn create_user_handler( .get_user_by_username(&body.username, UsersRelations::default()) .await? { - Some(u) => Err(warp::reject::custom(UserError::UserExists(u.id)))?, + Some(u) => Err(warp::reject::custom(UserError::AlreadyExists(u.id)))?, None => { if let Some(repositories) = body.repositories.clone() { for repo_id in repositories { - let result = db_access.get_repository(repo_id).await?; - if result.is_none() { - // TODO: improve - return Err(warp::reject::custom(RepositoryError::RepositoryNotFound( - repo_id, - ))); + if db_access.get_repository(repo_id).await?.is_none() { + return Err(warp::reject::custom(UserError::CannotBeCreated(format!( + "repository {repo_id} does not exist" + )))); } } } - Ok(json(&UserResponse::of(db_access.create_user(body).await?))) + let new_user = db_access.create_user(body).await?; + Ok(with_status( + json(&UserResponse::of(new_user)), + StatusCode::CREATED, + )) } } } pub async fn patch_user_handler( id: i32, body: PatchUser, - db_access: impl DBUser, + db_access: impl DBUser + DBRepository, ) -> Result { match db_access.get_user(id, UsersRelations::default()).await? { - Some(u) => Err(warp::reject::custom(UserError::UserExists(u.id)))?, - None => Ok(json(&UserResponse::of( - db_access.update_user_maintainers(id, body).await?, - ))), + None => Err(warp::reject::custom(UserError::NotFound(id)))?, + Some(_) => { + for repo_id in &body.repositories { + if db_access.get_repository(*repo_id).await?.is_none() { + return Err(warp::reject::custom(UserError::CannotBeUpdated( + id, + format!("repository {repo_id} does not exist"), + ))); + } + } + + db_access.update_user_maintainers(id, body).await?; + Ok(StatusCode::NO_CONTENT) + } } } @@ -66,7 +78,7 @@ pub async fn get_user_handler( }; match db_access.get_user(id, relations).await? { - None => Err(warp::reject::custom(UserError::UserNotFound(id)))?, + None => Err(warp::reject::custom(UserError::NotFound(id)))?, Some(user) => Ok(json(&UserResponse::of(user))), } } @@ -84,7 +96,7 @@ pub async fn get_user_by_name_handler( }; match db_access.get_user_by_username(&name, relations).await? { - None => Err(warp::reject::custom(UserError::UserNotFoundByName(name)))?, + None => Err(warp::reject::custom(UserError::NotFoundByName(name)))?, Some(user) => Ok(json(&UserResponse::of(user))), } } @@ -120,8 +132,8 @@ pub async fn delete_user_handler(id: i32, db_access: impl DBUser) -> Result { let _ = &db_access.delete_user(id).await?; - Ok(StatusCode::OK) + Ok(StatusCode::NO_CONTENT) } - None => Err(warp::reject::custom(UserError::UserNotFound(id)))?, + None => Err(warp::reject::custom(UserError::NotFound(id)))?, } } diff --git a/src/user/models.rs b/src/user/models.rs index e24d7ea..93d96e8 100644 --- a/src/user/models.rs +++ b/src/user/models.rs @@ -8,13 +8,16 @@ use warp::{ reply::{Reply, Response}, }; -use crate::handlers::ErrorResponse; +use crate::{ + db::utils::{defaul_sort_direction, sort_direction}, + error_handler::ErrorResponse, +}; #[derive(Deserialize)] pub struct User { pub id: i32, pub username: String, - // pub maintainers: Option>, + // TODO: add optional fields } #[derive(Serialize, Deserialize)] @@ -62,7 +65,6 @@ pub struct GetUserQuery { // pub has_tips: Option, // pub has_issues: Option, // pub has_wishes: Option, - // pub has_wishes: Option, } #[derive(Serialize, Deserialize)] @@ -78,13 +80,7 @@ impl UserSort { Ok(Self { field: format!("users.{field}"), - order: { - if descending { - "DESC".to_string() - } else { - "ASC".to_string() - } - }, + order: sort_direction(descending), }) } } @@ -93,7 +89,7 @@ impl Default for UserSort { fn default() -> Self { UserSort { field: "id".to_string(), - order: "ASC".to_string(), + order: defaul_sort_direction(), } } } diff --git a/src/user/routes.rs b/src/user/routes.rs index 10a913c..bda9146 100644 --- a/src/user/routes.rs +++ b/src/user/routes.rs @@ -4,7 +4,7 @@ use warp::filters::BoxedFilter; use warp::{Filter, Reply}; use crate::auth::with_auth; -use crate::http::{GetPagination, GetSort}; +use crate::pagination::{GetPagination, GetSort}; use crate::repository::db::DBRepository; use super::db::DBUser; @@ -21,7 +21,7 @@ pub fn routes(db_access: impl DBUser + DBRepository) -> BoxedFilter<(impl Reply, let user = warp::path!("users"); let user_id = warp::path!("users" / i32); let user_name = warp::path!("users" / "username" / String); - let user_maintainer = warp::path!("users" / "maintainers" / i32); + let user_maintainer = warp::path!("users" / i32 / "maintainers"); let get_users = user .and(warp::get()) From 3ccd5e3e58ffd1a7e4124bac52758a635debfef8 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 28 Apr 2024 18:29:31 -0300 Subject: [PATCH 12/98] feat: add endpoints and tess --- migrations/2024-04-13-204151_init/up.sql | 22 +- src/db/utils.rs | 2 +- src/error_handler.rs | 8 +- src/repository/db.rs | 111 ++++++- src/repository/errors.rs | 21 +- src/repository/handlers.rs | 91 ++++-- src/repository/models.rs | 108 ++++++- src/repository/routes.rs | 18 +- src/tests/repositories.rs | 388 ++++++++++++++++++++--- src/tests/users.rs | 115 ++----- src/user/handlers.rs | 14 +- src/user/models.rs | 4 +- 12 files changed, 709 insertions(+), 193 deletions(-) diff --git a/migrations/2024-04-13-204151_init/up.sql b/migrations/2024-04-13-204151_init/up.sql index 56db71a..18fbfa0 100644 --- a/migrations/2024-04-13-204151_init/up.sql +++ b/migrations/2024-04-13-204151_init/up.sql @@ -8,15 +8,13 @@ CREATE TABLE IF NOT EXISTS languages ( id SERIAL PRIMARY KEY, name VARCHAR(255) UNIQUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, - updated_at TIMESTAMP NULL -); --- used in the issues -CREATE TABLE IF NOT EXISTS labels ( - id SERIAL PRIMARY KEY, - name VARCHAR(255) UNIQUE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, - updated_at TIMESTAMP NULL -); + updated_at TIMESTAMP NULL -- used in the issues + CREATE TABLE IF NOT EXISTS labels ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) UNIQUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, + updated_at TIMESTAMP NULL + ); -- used in the repositories CREATE TABLE IF NOT EXISTS topics ( id SERIAL PRIMARY KEY, @@ -65,9 +63,9 @@ CREATE TABLE IF NOT EXISTS organizations ( -- basic github repository CREATE TABLE IF NOT EXISTS repositories ( id SERIAL PRIMARY KEY, - name VARCHAR(255), - url VARCHAR(255), - icon VARCHAR(255), + name VARCHAR(255) NOT NULL, + url VARCHAR(255) NOT NULL, + icon VARCHAR(255) NOT NULL, e_tag VARCHAR(40) NOT NULL, organization_id INT REFERENCES organizations(id), created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, diff --git a/src/db/utils.rs b/src/db/utils.rs index 407c45e..60b0682 100644 --- a/src/db/utils.rs +++ b/src/db/utils.rs @@ -87,6 +87,6 @@ pub fn sort_direction(descending: bool) -> String { } } -pub fn defaul_sort_direction() -> String { +pub fn default_sort_direction() -> String { ASC.to_string() } diff --git a/src/error_handler.rs b/src/error_handler.rs index 55511e3..7890ab7 100644 --- a/src/error_handler.rs +++ b/src/error_handler.rs @@ -7,8 +7,8 @@ use crate::{ auth_error::AuthenticationError, db::errors::DBError, organization::errors::OrganizationError, - pagination::PaginationError, - repository::errors::RepositoryError, + pagination::{PaginationError, SortError}, + repository::{errors::RepositoryError, models::RepositorySortError}, user::{errors::UserError, models::UserSortError}, }; @@ -27,6 +27,10 @@ pub async fn error_handler(err: Rejection) -> std::result::Result() { Ok(e.clone().into_response()) + } else if let Some(e) = err.find::() { + Ok(e.clone().into_response()) + } else if let Some(e) = err.find::() { + Ok(e.clone().into_response()) } else if let Some(e) = err.find::() { Ok(e.clone().into_response()) } else if let Some(e) = err.find::() { diff --git a/src/repository/db.rs b/src/repository/db.rs index eb6be95..2040bac 100644 --- a/src/repository/db.rs +++ b/src/repository/db.rs @@ -2,6 +2,7 @@ use mobc::async_trait; use mobc_postgres::tokio_postgres::Row; use warp::reject; +use super::models::{NewRepository, RepositoriesRelations, Repository, RepositorySort}; use crate::db::{ pool::DBAccess, utils::{ @@ -9,30 +10,58 @@ use crate::db::{ DB_QUERY_TIMEOUT, }, }; - -use super::models::{Repository, RepositoryRequest}; +use crate::pagination::GetPagination; const TABLE: &str = "repositories"; #[async_trait] pub trait DBRepository: Send + Sync + Clone + 'static { - async fn get_repository(&self, id: i32) -> Result, reject::Rejection>; + async fn get_repository( + &self, + id: i32, + relations: RepositoriesRelations, + ) -> Result, reject::Rejection>; async fn get_repository_by_name( &self, name: &str, + relations: RepositoriesRelations, ) -> Result, reject::Rejection>; - async fn get_repositories(&self) -> Result, reject::Rejection>; + async fn get_repositories( + &self, + relations: RepositoriesRelations, + pagination: GetPagination, + sort: RepositorySort, + ) -> Result, reject::Rejection>; async fn create_repository( &self, - repository: RepositoryRequest, + repository: NewRepository, ) -> Result; async fn delete_repository(&self, id: i32) -> Result<(), reject::Rejection>; } #[async_trait] impl DBRepository for DBAccess { - async fn get_repository(&self, id: i32) -> Result, reject::Rejection> { - let query = format!("SELECT * FROM {} WHERE id = $1", TABLE); + async fn get_repository( + &self, + id: i32, + relations: RepositoriesRelations, + ) -> Result, reject::Rejection> { + let mut query = format!("SELECT * FROM {} ", TABLE); + if relations.issues { + query += "LEFT JOIN issues on issues.repository_id = repositories.id "; + if relations.tips { + query += "LEFT JOIN tips on tips.id = issues.id "; + } + } + if relations.maintainers { + query += "LEFT JOIN maintainers on maintainers.repository_id = repositories.id "; + query += "LEFT JOIN users on maintainers.user_id = users.id "; + } + if relations.languages { + query += "LEFT JOIN languages on repositories.languages_id = languages.id "; + } + query += "WHERE id = $1"; + match query_opt_timeout(self, query.as_str(), &[&id], DB_QUERY_TIMEOUT).await? { Some(repository) => Ok(Some(row_to_repository(&repository))), None => Ok(None), @@ -41,32 +70,80 @@ impl DBRepository for DBAccess { async fn get_repository_by_name( &self, name: &str, + relations: RepositoriesRelations, ) -> Result, reject::Rejection> { - let query = format!("SELECT * FROM {} WHERE name = $1", TABLE); + let mut query = format!("SELECT * FROM {} ", TABLE); + if relations.issues { + query += "LEFT JOIN issues on issues.repository_id = repositories.id "; + if relations.tips { + query += "LEFT JOIN tips on tips.id = issues.id "; + } + } + if relations.maintainers { + query += "LEFT JOIN maintainers on maintainers.repository_id = repositories.id "; + query += "LEFT JOIN users on maintainers.user_id = users.id "; + } + if relations.languages { + query += "LEFT JOIN languages on repositories.languages_id = languages.id "; + } + query += "WHERE name = $1"; match query_opt_timeout(self, query.as_str(), &[&name], DB_QUERY_TIMEOUT).await? { Some(repository) => Ok(Some(row_to_repository(&repository))), None => Ok(None), } } - async fn get_repositories(&self) -> Result, reject::Rejection> { - let query = format!("SELECT * FROM {} ORDER BY created_at DESC", TABLE); - let rows = query_with_timeout(self, query.as_str(), &[], DB_QUERY_TIMEOUT).await?; + async fn get_repositories( + &self, + relations: RepositoriesRelations, + pagination: GetPagination, + sort: RepositorySort, + ) -> Result, reject::Rejection> { + let mut query = format!("SELECT * FROM {} ", TABLE); + if relations.issues { + query += "LEFT JOIN issues on issues.repository_id = repositories.id "; + if relations.tips { + query += "LEFT JOIN tips on tips.id = issues.id "; + } + } + if relations.maintainers { + query += "LEFT JOIN maintainers on maintainers.repository_id = repositories.id "; + query += "LEFT JOIN users on maintainers.user_id = users.id "; + } + if relations.languages { + query += "LEFT JOIN languages on repositories.languages_id = languages.id "; + } + query += &format!("ORDER BY {} {}", sort.field, sort.order); // cannot use $1 or $2 + + query += "LIMIT $1 OFFSET $2"; + let rows = query_with_timeout( + self, + query.as_str(), + &[&pagination.limit, &pagination.offset], + DB_QUERY_TIMEOUT, + ) + .await?; Ok(rows.iter().map(row_to_repository).collect()) } async fn create_repository( &self, - repository: RepositoryRequest, + repository: NewRepository, ) -> Result { let query = format!( - "INSERT INTO {} (name, organization_id) VALUES ($1, $2) RETURNING *", + "INSERT INTO {} (name, icon, organization_id, url, e_tag) VALUES ($1, $2, $3, $4, $5) RETURNING *", TABLE ); let row = query_one_timeout( self, &query, - &[&repository.name, &repository.organization_id], + &[ + &repository.name, + &repository.icon, + &repository.organization_id, + &repository.url, + &repository.e_tag, + ], DB_QUERY_TIMEOUT, ) .await?; @@ -83,9 +160,15 @@ fn row_to_repository(row: &Row) -> Repository { let id: i32 = row.get(0); let name: &str = row.get(1); let organization_id: i32 = row.get(2); + let icon: &str = row.get(3); + let url: &str = row.get(4); + let e_tag: &str = row.get(5); Repository { id, name: name.to_string(), organization_id, + icon: icon.to_string(), + url: url.to_string(), + e_tag: e_tag.to_string(), } } diff --git a/src/repository/errors.rs b/src/repository/errors.rs index 1212c3e..9d1ac10 100644 --- a/src/repository/errors.rs +++ b/src/repository/errors.rs @@ -12,18 +12,22 @@ use crate::error_handler::ErrorResponse; #[derive(Clone, Error, Debug, Deserialize, PartialEq)] pub enum RepositoryError { - RepositoryExists(i32), - RepositoryNotFound(i32), + AlreadyExists(i32), + NotFound(i32), + NotFoundByName(String), } impl fmt::Display for RepositoryError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - RepositoryError::RepositoryExists(id) => { - write!(f, "Repository #{} already exists", id) + RepositoryError::AlreadyExists(id) => { + write!(f, "Repository #{id} already exists") } - RepositoryError::RepositoryNotFound(id) => { - write!(f, "Repository #{} not found", id) + RepositoryError::NotFound(id) => { + write!(f, "Repository #{id} not found") + } + RepositoryError::NotFoundByName(name) => { + write!(f, "Repository {name} not found") } } } @@ -34,8 +38,9 @@ impl Reject for RepositoryError {} impl Reply for RepositoryError { fn into_response(self) -> Response { let code = match self { - RepositoryError::RepositoryExists(_) => StatusCode::BAD_REQUEST, - RepositoryError::RepositoryNotFound(_) => StatusCode::NOT_FOUND, + RepositoryError::AlreadyExists(_) => StatusCode::BAD_REQUEST, + RepositoryError::NotFound(_) => StatusCode::NOT_FOUND, + RepositoryError::NotFoundByName(_) => StatusCode::NOT_FOUND, }; let message = self.to_string(); diff --git a/src/repository/handlers.rs b/src/repository/handlers.rs index 91e25bd..a9bb85b 100644 --- a/src/repository/handlers.rs +++ b/src/repository/handlers.rs @@ -1,29 +1,38 @@ use warp::{ http::StatusCode, reject::Rejection, - reply::{json, Reply}, + reply::{json, with_status, Reply}, }; -use crate::organization::{db::DBOrganization, errors::OrganizationError}; +use crate::{ + organization::{db::DBOrganization, errors::OrganizationError}, + pagination::GetSort, +}; use super::{ db::DBRepository, errors::RepositoryError, - models::{RepositoryRequest, RepositoryResponse}, + models::{GetRepositoryQuery, NewRepository, RepositoriesRelations, RepositoryResponse}, }; +use crate::pagination::GetPagination; +use crate::repository::models::RepositorySort; pub async fn create_repository_handler( - body: RepositoryRequest, + body: NewRepository, db_access: impl DBRepository + DBOrganization, ) -> Result { match db_access.get_organization(body.organization_id).await? { - Some(_) => match db_access.get_repository_by_name(&body.name).await? { - Some(u) => Err(warp::reject::custom(RepositoryError::RepositoryExists( - u.id, - )))?, - None => Ok(json(&RepositoryResponse::of( - db_access.create_repository(body).await?, - ))), + Some(_) => match db_access + .get_repository_by_name(&body.name, RepositoriesRelations::default()) + .await? + { + Some(u) => Err(warp::reject::custom(RepositoryError::AlreadyExists(u.id)))?, + None => Ok(with_status( + json(&RepositoryResponse::of( + db_access.create_repository(body).await?, + )), + StatusCode::CREATED, + )), }, None => Err(warp::reject::custom( OrganizationError::OrganizationNotFound(body.organization_id), @@ -34,19 +43,58 @@ pub async fn create_repository_handler( pub async fn get_repository_handler( id: i32, db_access: impl DBRepository, + query: GetRepositoryQuery, ) -> Result { - match db_access.get_repository(id).await? { - None => Err(warp::reject::custom(RepositoryError::RepositoryNotFound( - id, - )))?, + let relations = RepositoriesRelations { + tips: query.tips.unwrap_or_default(), + maintainers: query.maintainers.unwrap_or_default(), + issues: query.issues.unwrap_or_default(), + languages: query.languages.unwrap_or_default(), + }; + match db_access.get_repository(id, relations).await? { + None => Err(warp::reject::custom(RepositoryError::NotFound(id)))?, + Some(repository) => Ok(json(&RepositoryResponse::of(repository))), + } +} +pub async fn get_repository_handler_name( + name: String, + db_access: impl DBRepository, + query: GetRepositoryQuery, +) -> Result { + let relations = RepositoriesRelations { + tips: query.tips.unwrap_or_default(), + maintainers: query.maintainers.unwrap_or_default(), + issues: query.issues.unwrap_or_default(), + languages: query.languages.unwrap_or_default(), + }; + match db_access.get_repository_by_name(&name, relations).await? { + None => Err(warp::reject::custom(RepositoryError::NotFoundByName(name)))?, Some(repository) => Ok(json(&RepositoryResponse::of(repository))), } } pub async fn get_repositories_handler( db_access: impl DBRepository, + query: GetRepositoryQuery, + filters: GetPagination, + sort: GetSort, ) -> Result { - let repositories = db_access.get_repositories().await?; + let relations = RepositoriesRelations { + tips: query.tips.unwrap_or_default(), + maintainers: query.maintainers.unwrap_or_default(), + issues: query.issues.unwrap_or_default(), + languages: query.languages.unwrap_or_default(), + }; + let pagination = filters.validate()?; + let sort = sort.validate()?; + let repository_sort = match (sort.sort_by, sort.descending) { + (Some(sort_by), Some(descending)) => RepositorySort::new(&sort_by, descending)?, + _ => RepositorySort::default(), + }; + + let repositories = db_access + .get_repositories(relations, pagination, repository_sort) + .await?; Ok(json::>( &repositories .into_iter() @@ -59,13 +107,14 @@ pub async fn delete_repository_handler( id: i32, db_access: impl DBRepository, ) -> Result { - match db_access.get_repository(id).await? { + match db_access + .get_repository(id, RepositoriesRelations::default()) + .await? + { Some(_) => { let _ = &db_access.delete_repository(id).await?; - Ok(StatusCode::OK) + Ok(StatusCode::NO_CONTENT) } - None => Err(warp::reject::custom(RepositoryError::RepositoryNotFound( - id, - )))?, + None => Err(warp::reject::custom(RepositoryError::NotFound(id)))?, } } diff --git a/src/repository/models.rs b/src/repository/models.rs index 3983468..3355b7c 100644 --- a/src/repository/models.rs +++ b/src/repository/models.rs @@ -1,16 +1,34 @@ +use std::fmt; + use serde_derive::{Deserialize, Serialize}; +use warp::{ + http::StatusCode, + reject::Reject, + reply::{Reply, Response}, +}; + +use crate::{ + db::utils::{default_sort_direction, sort_direction}, + error_handler::ErrorResponse, +}; +use thiserror::Error; #[derive(Deserialize)] pub struct Repository { pub id: i32, pub name: String, pub organization_id: i32, + pub icon: String, + pub url: String, + pub e_tag: String, } - #[derive(Serialize, Deserialize)] -pub struct RepositoryRequest { +pub struct NewRepository { pub name: String, + pub icon: String, pub organization_id: i32, + pub url: String, + pub e_tag: String, } #[derive(Serialize, Deserialize, PartialEq, Debug)] @@ -18,6 +36,9 @@ pub struct RepositoryResponse { pub id: i32, pub name: String, pub organization_id: i32, + pub icon: String, + pub url: String, + pub e_tag: String, } impl RepositoryResponse { @@ -26,6 +47,89 @@ impl RepositoryResponse { id: repository.id, name: repository.name, organization_id: repository.organization_id, + icon: repository.icon, + url: repository.url, + e_tag: repository.e_tag, + } + } +} + +#[derive(Default)] +pub struct RepositoriesRelations { + pub issues: bool, + pub tips: bool, + pub maintainers: bool, + pub languages: bool, +} +// query args + +#[derive(Serialize, Deserialize, Default)] +pub struct GetRepositoryQuery { + pub languages: Option, + pub tips: Option, + pub maintainers: Option, + pub issues: Option, + // TODO: add filters + // pub is_maintainer: Option, + // pub has_tips: Option, + // pub has_issues: Option, + // pub has_wishes: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct RepositorySort { + pub field: String, + pub order: String, +} +impl RepositorySort { + pub fn new(field: &str, descending: bool) -> Result { + if field != "id" && field != "name" { + return Err(RepositorySortError::InvalidSortBy(field.to_owned())); + } + + Ok(Self { + field: format!("repository.{field}"), + order: sort_direction(descending), + }) + } +} + +impl Default for RepositorySort { + fn default() -> Self { + RepositorySort { + field: "id".to_string(), + order: default_sort_direction(), } } } + +#[derive(Clone, Error, Debug, Deserialize, PartialEq)] +pub enum RepositorySortError { + InvalidSortBy(String), +} + +impl fmt::Display for RepositorySortError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RepositorySortError::InvalidSortBy(field) => { + write!(f, "Sort by {} is invalid", field) + } + } + } +} + +impl Reject for RepositorySortError {} + +impl Reply for RepositorySortError { + fn into_response(self) -> Response { + let status_code = match self { + RepositorySortError::InvalidSortBy(_) => StatusCode::BAD_REQUEST, + }; + let code = status_code; + let message = self.to_string(); + + let json = warp::reply::json(&ErrorResponse { message }); + + warp::reply::with_status(json, code).into_response() + } +} diff --git a/src/repository/routes.rs b/src/repository/routes.rs index 08cbdb8..30183bc 100644 --- a/src/repository/routes.rs +++ b/src/repository/routes.rs @@ -8,6 +8,9 @@ use crate::organization::db::DBOrganization; use super::db::DBRepository; use super::handlers; +use super::models::GetRepositoryQuery; +use crate::pagination::GetPagination; +use crate::pagination::GetSort; fn with_db( db_pool: impl DBRepository + DBOrganization, @@ -18,16 +21,26 @@ fn with_db( pub fn routes(db_access: impl DBRepository + DBOrganization) -> BoxedFilter<(impl Reply,)> { let repository = warp::path!("repositories"); // TODO: move this to the "organization" endpoint as a subendpoint let repository_id = warp::path!("repositories" / i32); + let repository_name = warp::path!("repositories" / "name" / String); let get_repositories = repository .and(warp::get()) .and(with_db(db_access.clone())) + .and(warp::query::()) + .and(warp::query::()) + .and(warp::query::()) .and_then(handlers::get_repositories_handler); + let get_repository_by_name = repository_name + .and(warp::get()) + .and(with_db(db_access.clone())) + .and(warp::query::()) + .and_then(handlers::get_repository_handler_name); + let get_repository = repository_id .and(warp::get()) - // .and(warp::path::param()) .and(with_db(db_access.clone())) + .and(warp::query::()) .and_then(handlers::get_repository_handler); let create_repository = repository @@ -46,7 +59,8 @@ pub fn routes(db_access: impl DBRepository + DBOrganization) -> BoxedFilter<(imp let route = get_repositories .or(get_repository) .or(create_repository) - .or(delete_repository); + .or(delete_repository) + .or(get_repository_by_name); route.boxed() } diff --git a/src/tests/repositories.rs b/src/tests/repositories.rs index 1f8997c..97afef7 100644 --- a/src/tests/repositories.rs +++ b/src/tests/repositories.rs @@ -1,83 +1,385 @@ #[cfg(test)] pub mod tests { - use crate::repository::{ - db::DBRepository, - models::{Repository, RepositoryRequest}, + use crate::error_handler::{error_handler, ErrorResponse}; + use crate::organization::db::DBOrganization; + use crate::organization::models::{Organization, OrganizationRequest}; + use crate::pagination::GetPagination; + use crate::repository::models::{ + NewRepository, RepositoriesRelations, RepositoryResponse, RepositorySort, }; + use crate::repository::routes::routes; + use crate::repository::{db::DBRepository, models::Repository}; use mobc::async_trait; - use warp::reject; + use warp::test::request; + use warp::{reject, Filter}; #[derive(Clone)] - pub struct RepositoriesDBMockValues {} - #[derive(Clone)] - pub struct RepositoriesDBMockEmpty {} + pub struct RepositoriesDBMock {} #[async_trait] - impl DBRepository for RepositoriesDBMockValues { - async fn get_repository(&self, id: i32) -> Result, reject::Rejection> { - Ok(Some(Repository { - id, - name: "repo".to_owned(), - organization_id: 1, - })) + impl DBRepository for RepositoriesDBMock { + async fn get_repository( + &self, + id: i32, + relations: RepositoriesRelations, + ) -> Result, reject::Rejection> { + if id == 1 { + Ok(Some(Repository { + id, + name: "repo".to_owned(), + organization_id: 1, + icon: "icon".to_string(), + url: "url".to_string(), + e_tag: "e_tag".to_string(), + })) + } else { + Ok(None) + } } async fn get_repository_by_name( &self, name: &str, + relations: RepositoriesRelations, ) -> Result, reject::Rejection> { - Ok(Some(Repository { - id: 1, - name: name.to_owned(), - organization_id: 1, - })) + if name == "not_found" || name == "new" { + Ok(None) + } else { + Ok(Some(Repository { + id: 1, + name: "repo".to_owned(), + organization_id: 1, + icon: "icon".to_string(), + url: "url".to_string(), + e_tag: "e_tag".to_string(), + })) + } } - async fn get_repositories(&self) -> Result, reject::Rejection> { - Ok(vec![Repository { - name: "repo".to_owned(), - organization_id: 1, - id: 1, - }]) + async fn get_repositories( + &self, + relations: RepositoriesRelations, + pagination: GetPagination, + sort: RepositorySort, + ) -> Result, reject::Rejection> { + Ok(vec![]) } async fn create_repository( &self, - repository: RepositoryRequest, + repository: NewRepository, ) -> Result { Ok(Repository { name: repository.name.to_string(), id: 1, organization_id: repository.organization_id, + icon: repository.icon, + url: repository.url, + e_tag: repository.e_tag, }) } - async fn delete_repository(&self, _: i32) -> Result<(), reject::Rejection> { + async fn delete_repository(&self, id: i32) -> Result<(), reject::Rejection> { Ok(()) } } + #[async_trait] - impl DBRepository for RepositoriesDBMockEmpty { - async fn get_repository(&self, _: i32) -> Result, reject::Rejection> { - Ok(None) + impl DBOrganization for RepositoriesDBMock { + async fn get_organization( + &self, + id: i32, + ) -> Result, reject::Rejection> { + Ok(Some(Organization { + id, + name: "ok".to_string(), + })) } - async fn get_repository_by_name( + async fn get_organization_by_name( &self, - _: &str, - ) -> Result, reject::Rejection> { - Ok(None) + name: &str, + ) -> Result, reject::Rejection> { + Ok(Some(Organization { + id: 1, + name: name.to_string(), + })) } - async fn get_repositories(&self) -> Result, reject::Rejection> { - Ok(vec![]) + async fn get_organizations(&self) -> Result, reject::Rejection> { + Ok(vec![ + (Organization { + id: 1, + name: "name".to_string(), + }), + ]) } - async fn create_repository( + async fn create_organization( &self, - repository: RepositoryRequest, - ) -> Result { - Ok(Repository { - name: repository.name.to_string(), + organization: OrganizationRequest, + ) -> Result { + Ok(Organization { id: 1, - organization_id: repository.organization_id, + name: organization.name, }) } - async fn delete_repository(&self, _: i32) -> Result<(), reject::Rejection> { + async fn delete_organization(&self, id: i32) -> Result<(), reject::Rejection> { Ok(()) } } + #[tokio::test] + async fn test_get_repo_by_id_not_found() { + let id = 2; + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request() + .path(&format!("/repositories/{id}")) + .reply(&r) + .await; + assert_eq!(resp.status(), 404); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); + + assert_eq!( + response, + ErrorResponse { + message: format!("Repository #{id} not found",), + } + ) + } + #[tokio::test] + async fn test_get_repository_by_id_exists() { + let id = 1; + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request() + .path(&format!("/repositories/{id}")) + .reply(&r) + .await; + assert_eq!(resp.status(), 200); + let body = resp.into_body(); + let response: RepositoryResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!( + response, + RepositoryResponse { + id, + name: "repo".to_owned(), + organization_id: 1, + icon: "icon".to_string(), + url: "url".to_string(), + e_tag: "e_tag".to_string(), + } + ) + } + #[tokio::test] + async fn test_get_repository_by_name_not_found() { + let name = "not_found"; + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request() + .path(&format!("/repositories/name/{name}")) + .reply(&r) + .await; + assert_eq!(resp.status(), 404); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!( + response, + ErrorResponse { + message: format!("Repository {name} not found",), + } + ) + } + #[tokio::test] + async fn test_get_repository_by_name_exists() { + let name = "repo"; + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request() + .path(&format!("/repositories/name/{name}")) + .reply(&r) + .await; + assert_eq!(resp.status(), 200); + let body = resp.into_body(); + let expected_response = RepositoryResponse { + id: 1, + name: "repo".to_owned(), + organization_id: 1, + icon: "icon".to_string(), + url: "url".to_string(), + e_tag: "e_tag".to_string(), + }; + let response: RepositoryResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!(response, expected_response) + } + #[tokio::test] + async fn test_get_repositories() { + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request().path(&format!("/repositories")).reply(&r).await; + assert_eq!(resp.status(), 200); + let body = resp.into_body(); + let response: Vec = serde_json::from_slice(&body).unwrap(); + assert_eq!(response, vec![]); + } + #[tokio::test] + async fn test_get_repositories_valid_query_params() { + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request().path(&format!("/repositories?languages=false&tips=false&maintainers=false&issues=false&limit=1&offset=10&sort_by=id&descending=false")).reply(&r).await; + assert_eq!(resp.status(), 200); + let body = resp.into_body(); + let response: Vec = serde_json::from_slice(&body).unwrap(); + assert_eq!(response, vec![]); + } + #[tokio::test] + async fn test_get_repositories_invalid_query_params() { + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request() + .path(&format!("/repositories?languages=fal1se")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); // TODO: check why it's a 401 + let resp = request() + .path(&format!("/repositories?maintainers=fal1se")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); // TODO: check why it's a 401 + let resp = request() + .path(&format!("/repositories?issues=123")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); // TODO: check why it's a 401 + let resp = request() + .path(&format!("/repositories?tips=123")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); + let resp = request() + .path(&format!("/repositories?limit=asd")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); + let resp = request() + .path(&format!("/repositories?offset=asd")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); + let resp = request() + .path(&format!("/repositories?sort_by=invalid")) + .reply(&r) + .await; + assert_eq!(resp.status(), 400); + let resp = request() + .path(&format!("/repositories?descending=sadf")) + .reply(&r) + .await; + assert_eq!(resp.status(), 401); // TODO: check why it's a 401 + } + + #[tokio::test] + async fn test_create_repository_ok() { + let id = 1; + let name = "new".to_string(); + let icon = "icon".to_string(); + let e_tag = "e_tag".to_string(); + let url = "url".to_string(); + + let new_repository: Vec = serde_json::to_vec(&NewRepository { + name: name.clone(), + icon: icon.clone(), + organization_id: 1, + url: url.clone(), + e_tag: e_tag.clone(), + }) + .unwrap(); + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request() + .body(new_repository) + .path(&"/repositories") + .header("Authorization", "Basic dGVzdDp0ZXN0") // test:test + .method("POST") + .reply(&r) + .await; + assert_eq!(resp.status(), 201); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let expected_response = RepositoryResponse { + id, + name, + organization_id: 1, + icon, + url, + e_tag, + }; + let response: RepositoryResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!(response, expected_response) + } + + #[tokio::test] + async fn test_create_repository_already_exists() { + let name = "name".to_string(); + let icon = "icon".to_string(); + let e_tag = "e_tag".to_string(); + let url = "url".to_string(); + + let new_repository: Vec = serde_json::to_vec(&NewRepository { + name: name.clone(), + icon: icon.clone(), + organization_id: 1, + url: url.clone(), + e_tag: e_tag.clone(), + }) + .unwrap(); + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request() + .body(new_repository) + .path(&"/repositories") + .header("Authorization", "Basic dGVzdDp0ZXN0") // test:test + .method("POST") + .reply(&r) + .await; + assert_eq!(resp.status(), 400); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!( + response, + ErrorResponse { + message: format!("Repository #1 already exists",), + } + ) + } + + #[tokio::test] + async fn test_delete_repository() { + let id = 1; + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request() + .path(&format!("/repositories/{id}")) + .method("DELETE") + .header("Authorization", "Basic dGVzdDp0ZXN0") // test:test + .reply(&r) + .await; + assert_eq!(resp.status(), 204); + let body = resp.into_body(); + assert!(body.is_empty()); + } + + #[tokio::test] + async fn test_delete_repository_does_not_exist_mock_db() { + let id = 4; + + let r = routes(RepositoriesDBMock {}).recover(error_handler); + let resp = request() + .path(&format!("/repositories/4")) + .method("DELETE") + .header("Authorization", "Basic dGVzdDp0ZXN0") // test:test + .reply(&r) + .await; + + assert_eq!(resp.status(), 404); + let body = resp.into_body(); + assert!(!body.is_empty()); + + let expected_response = ErrorResponse { + message: format!("Repository #{id} not found"), + }; + let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!(response, expected_response) + } } diff --git a/src/tests/users.rs b/src/tests/users.rs index e4a595c..f67025c 100644 --- a/src/tests/users.rs +++ b/src/tests/users.rs @@ -5,86 +5,22 @@ mod tests { pagination::GetPagination, repository::{ db::DBRepository, - models::{Repository, RepositoryRequest}, + models::{NewRepository, RepositoriesRelations, Repository, RepositorySort}, }, user::{ - self, db::DBUser, models::{NewUser, PatchUser, User, UserResponse, UserSort, UsersRelations}, routes::routes, }, }; - use diesel::expression::is_aggregate::No; use mobc::async_trait; use warp::{reject, test::request, Filter}; #[derive(Clone)] - pub struct UsersDBMockValues {} - #[derive(Clone)] - pub struct UsersDBMockEmpty {} - - #[derive(Clone)] - pub struct UsersAndRepositoriesDBMockValues {} + pub struct UsersDBMock {} #[async_trait] - impl DBUser for UsersDBMockValues { - async fn get_user( - &self, - id: i32, - relations: UsersRelations, - ) -> Result, reject::Rejection> { - if id == 1 { - Ok(Some(User { - id: 1, - username: "username".to_string(), - })) - } else { - Ok(None) - } - } - async fn get_user_by_username( - &self, - username: &str, - relations: UsersRelations, - ) -> Result, reject::Rejection> { - Ok(Some(User { - id: 1, - username: username.to_string(), - })) - } - async fn get_users( - &self, - relations: UsersRelations, - pagination: GetPagination, - sort: UserSort, - ) -> Result, reject::Rejection> { - Ok(vec![User { - id: 1, - username: "username".to_string(), - }]) - } - async fn create_user(&self, user: NewUser) -> Result { - Ok(User { - id: 1, - username: "username".to_string(), - }) - } - async fn update_user_maintainers( - &self, - id: i32, - user: PatchUser, - ) -> Result { - Ok(User { - id: 1, - username: "username".to_string(), - }) - } - async fn delete_user(&self, _: i32) -> Result<(), reject::Rejection> { - Ok(()) - } - } - #[async_trait] - impl DBUser for UsersDBMockEmpty { + impl DBUser for UsersDBMock { async fn get_user( &self, id: i32, @@ -143,27 +79,40 @@ mod tests { } #[async_trait] - impl DBRepository for UsersDBMockEmpty { - async fn get_repository(&self, _: i32) -> Result, reject::Rejection> { + impl DBRepository for UsersDBMock { + async fn get_repository( + &self, + _: i32, + _: RepositoriesRelations, + ) -> Result, reject::Rejection> { Ok(None) } async fn get_repository_by_name( &self, _: &str, + _: RepositoriesRelations, ) -> Result, reject::Rejection> { Ok(None) } - async fn get_repositories(&self) -> Result, reject::Rejection> { + async fn get_repositories( + &self, + _: RepositoriesRelations, + _: GetPagination, + _: RepositorySort, + ) -> Result, reject::Rejection> { Ok(vec![]) } async fn create_repository( &self, - _: RepositoryRequest, + _: NewRepository, ) -> Result { Ok(Repository { id: 1, name: "repo".to_owned(), organization_id: 1, + icon: "icon".to_string(), + url: "url".to_string(), + e_tag: "e_tag".to_string(), }) } async fn delete_repository(&self, _: i32) -> Result<(), reject::Rejection> { @@ -173,7 +122,7 @@ mod tests { #[tokio::test] async fn test_get_user_by_id_not_found() { let id = 2; - let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(error_handler::error_handler); let resp = request().path(&format!("/users/{id}")).reply(&r).await; assert_eq!(resp.status(), 404); let body = resp.into_body(); @@ -191,7 +140,7 @@ mod tests { #[tokio::test] async fn test_get_user_by_id_exists() { let id = 1; - let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(error_handler::error_handler); let resp = request().path(&format!("/users/{id}")).reply(&r).await; assert_eq!(resp.status(), 200); let body = resp.into_body(); @@ -207,7 +156,7 @@ mod tests { #[tokio::test] async fn test_get_user_by_name_not_found() { let name = "not_found"; - let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(error_handler::error_handler); let resp = request() .path(&format!("/users/username/{name}")) .reply(&r) @@ -227,7 +176,7 @@ mod tests { #[tokio::test] async fn test_get_user_by_name_exists() { let name = "username"; - let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(error_handler::error_handler); let resp = request() .path(&format!("/users/username/{name}")) .reply(&r) @@ -243,7 +192,7 @@ mod tests { } #[tokio::test] async fn test_get_users() { - let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(error_handler::error_handler); let resp = request().path(&format!("/users")).reply(&r).await; assert_eq!(resp.status(), 200); let body = resp.into_body(); @@ -252,7 +201,7 @@ mod tests { } #[tokio::test] async fn test_get_users_valid_query_params() { - let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(error_handler::error_handler); let resp = request().path(&format!("/users?wishes=false&tips=false&maintainers=false&issues=false&limit=1&offset=10&sort_by=id&descending=false")).reply(&r).await; assert_eq!(resp.status(), 200); let body = resp.into_body(); @@ -261,7 +210,7 @@ mod tests { } #[tokio::test] async fn test_get_users_invalid_query_params() { - let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(error_handler::error_handler); let resp = request() .path(&format!("/users?wishes=fal1se")) .reply(&r) @@ -307,7 +256,7 @@ mod tests { repositories: None, }) .unwrap(); - let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(error_handler::error_handler); let resp = request() .body(new_user) .path(&"/users") @@ -330,7 +279,7 @@ mod tests { repositories: vec![1], }) .unwrap(); - let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(error_handler::error_handler); let resp = request() .body(new_user) .path(&"/users/1/maintainers") @@ -358,7 +307,7 @@ mod tests { repositories: None, }) .unwrap(); - let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(error_handler::error_handler); let resp = request() .body(new_user) .path(&"/users") @@ -382,7 +331,7 @@ mod tests { #[tokio::test] async fn test_delete_user() { let id = 1; - let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(error_handler::error_handler); let resp = request() .path(&format!("/users/{id}")) .method("DELETE") @@ -398,7 +347,7 @@ mod tests { async fn test_delete_user_does_not_exist_mock_db() { let id = 4; - let r = routes(UsersDBMockEmpty {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(error_handler::error_handler); let resp = request() .path(&format!("/users/4")) .method("DELETE") diff --git a/src/user/handlers.rs b/src/user/handlers.rs index 0c9b8ff..5fad12b 100644 --- a/src/user/handlers.rs +++ b/src/user/handlers.rs @@ -6,7 +6,7 @@ use warp::{ use crate::{ pagination::{GetPagination, GetSort}, - repository::db::DBRepository, + repository::{db::DBRepository, models::RepositoriesRelations}, }; use super::{ @@ -27,7 +27,11 @@ pub async fn create_user_handler( None => { if let Some(repositories) = body.repositories.clone() { for repo_id in repositories { - if db_access.get_repository(repo_id).await?.is_none() { + if db_access + .get_repository(repo_id, RepositoriesRelations::default()) + .await? + .is_none() + { return Err(warp::reject::custom(UserError::CannotBeCreated(format!( "repository {repo_id} does not exist" )))); @@ -51,7 +55,11 @@ pub async fn patch_user_handler( None => Err(warp::reject::custom(UserError::NotFound(id)))?, Some(_) => { for repo_id in &body.repositories { - if db_access.get_repository(*repo_id).await?.is_none() { + if db_access + .get_repository(*repo_id, RepositoriesRelations::default()) + .await? + .is_none() + { return Err(warp::reject::custom(UserError::CannotBeUpdated( id, format!("repository {repo_id} does not exist"), diff --git a/src/user/models.rs b/src/user/models.rs index 93d96e8..f863383 100644 --- a/src/user/models.rs +++ b/src/user/models.rs @@ -9,7 +9,7 @@ use warp::{ }; use crate::{ - db::utils::{defaul_sort_direction, sort_direction}, + db::utils::{default_sort_direction, sort_direction}, error_handler::ErrorResponse, }; @@ -89,7 +89,7 @@ impl Default for UserSort { fn default() -> Self { UserSort { field: "id".to_string(), - order: defaul_sort_direction(), + order: default_sort_direction(), } } } From a02bef211ead067fa90388deda9f5e057b9f7592 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 28 Apr 2024 18:35:15 -0300 Subject: [PATCH 13/98] feat: test CI --- .github/workflows/tests.yml | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..5ce2c77 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,39 @@ +name: PR to Main Workflow + +on: + pull_request: + branches: + - main + types: [opened] + +env: + RUSTFLAGS: "-Dwarnings" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + + # - name: Run Clippy + # run: | + # cargo clippy --all-targets --all-features + + - name: Test code + run: | + cargo test --verbose From 7b9bcdb73634a1d5158048779b6d0682e7cfec1e Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 28 Apr 2024 18:36:08 -0300 Subject: [PATCH 14/98] fix: test CI --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5ce2c77..cc0ebf5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,7 +4,7 @@ on: pull_request: branches: - main - types: [opened] + types: [opened, synchronize] env: RUSTFLAGS: "-Dwarnings" From 3809300fa6fe51c5b42085e33ec5c75d2f9c3bf5 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 28 Apr 2024 18:36:36 -0300 Subject: [PATCH 15/98] fix: test CI name --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cc0ebf5..9621a1d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: PR to Main Workflow +name: Lint and test on: pull_request: From b170004750f3381e25fd79151908a3808cef20d0 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 28 Apr 2024 18:39:04 -0300 Subject: [PATCH 16/98] fix: test CI name --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9621a1d..f551c7b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,8 +6,8 @@ on: - main types: [opened, synchronize] -env: - RUSTFLAGS: "-Dwarnings" +# env: +# RUSTFLAGS: "-Dwarnings" jobs: build: From 5b3560bf2be176589ab4510b12afec11b2be3b62 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 28 Apr 2024 18:41:38 -0300 Subject: [PATCH 17/98] fix: deploy --- .github/workflows/prod.yml | 29 ++++------------------- .github/workflows/stage.yml | 46 +++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/stage.yml diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 9e4a9d9..fbde5df 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -1,9 +1,8 @@ -name: Build, Test and release to Prod +name: Production on: - push: - branches: - - main + release: + types: [created] jobs: build: @@ -13,24 +12,6 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Cache dependencies - uses: actions/cache@v3 - with: - path: | - ~/.cargo - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - toolchain: stable - - - name: Build and test code - run: | - cargo build --verbose - cargo test --verbose - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 @@ -42,5 +23,5 @@ jobs: - name: Build and push Docker image run: | - docker build -t ${{ vars.DOCKER_REGISTRY }}:${{ github.sha }} -f Dockerfile . - docker push ${{ vars.DOCKER_REGISTRY }}:${{ github.sha }} + docker build -t ${{ secrets.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} -f Dockerfile . + docker push ${{ secrets.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} diff --git a/.github/workflows/stage.yml b/.github/workflows/stage.yml new file mode 100644 index 0000000..9219f15 --- /dev/null +++ b/.github/workflows/stage.yml @@ -0,0 +1,46 @@ +name: Stage + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + + - name: Build and test code + run: | + cargo build --verbose + cargo test --verbose + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push Docker image + run: | + docker build -t ${{ vars.DOCKER_REGISTRY }}:${{ github.sha }} -f Dockerfile . + docker push ${{ vars.DOCKER_REGISTRY }}:${{ github.sha }} From 34b32547d3d9a78e7987bd65d5f2166030f67103 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 28 Apr 2024 18:43:15 -0300 Subject: [PATCH 18/98] fix: deploy --- .github/workflows/prod.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index fbde5df..69b2aa2 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -23,5 +23,5 @@ jobs: - name: Build and push Docker image run: | - docker build -t ${{ secrets.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} -f Dockerfile . - docker push ${{ secrets.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} + docker build -t ${{ vars.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} -f Dockerfile . + docker push ${{ vars.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} From 1dccb79e6ee836b9e801717499c8c37bd9dab2a2 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 28 Apr 2024 19:56:11 -0300 Subject: [PATCH 19/98] fix: CI --- .github/workflows/tests.yml | 8 ++++++++ Dockerfile | 1 - 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f551c7b..06a3fb8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -34,6 +34,14 @@ jobs: # run: | # cargo clippy --all-targets --all-features + - name: Build code + run: | + cargo build --verbose + - name: Test code run: | cargo test --verbose + + - name: Build docker image + run: | + docker build -t ${{ vars.DOCKER_REGISTRY }}:${{ github.sha }} -f Dockerfile . diff --git a/Dockerfile b/Dockerfile index c22c9f7..f0743c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,7 +30,6 @@ RUN adduser \ USER appuser COPY --from=build /bin/kudos_api /bin/ -COPY db.sql db.sql EXPOSE ${SERVER_PORT} From 26191b74e92ef8be7d04501185a64df0582231f2 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 28 Apr 2024 20:18:32 -0300 Subject: [PATCH 20/98] fix: env var --- .env.example | 4 +- Makefile | 13 +- README.md | 2 +- docker-compose.yaml | 4 +- migrations/2024-04-13-204151_init/up.sql | 22 +- src/schema.rs | 243 +++++++++++++++++++++++ src/types.rs | 7 +- 7 files changed, 269 insertions(+), 26 deletions(-) create mode 100644 src/schema.rs diff --git a/.env.example b/.env.example index c9b8fe4..b44c805 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ DATABASE_URL=postgres://postgres:password@localhost:5432/database -HTTP_SERVER_HOST=0.0.0.0 -HTTP_SERVER_PORT=8000 +HOST=0.0.0.0 +PORT=8000 USERNAME=test PASSWORD=test \ No newline at end of file diff --git a/Makefile b/Makefile index 79ac86b..2efa461 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,8 @@ -DATABASE_URL=postgres://postgres:password@localhost:5432/database -DATABASE_INIT_FILE=db.sql -HTTP_SERVER_HOST=0.0.0.0 -HTTP_SERVER_PORT=8000 -USERNAME=test -PASSWORD=test +DATABASE_URL?=postgres://postgres:password@localhost:5432/database +HOST?=0.0.0.0 +PORT?=8000 +USERNAME?=test +PASSWORD?=test DOCKER_DB_CONTAINER_NAME:=db DOCKER_COMPOSE:=docker-compose DOCKER_COMPOSE_FILE:=docker-compose.yaml @@ -12,7 +11,7 @@ DOCKER_COMPOSE_FILE:=docker-compose.yaml .PHONY: run run: - USERNAME="$(USERNAME)" PASSWORD="$(PASSWORD)" DATABASE_URL="$(DATABASE_URL)" HTTP_SERVER_HOST="$(HTTP_SERVER_HOST)" HTTP_SERVER_PORT=$(HTTP_SERVER_PORT) cargo run + USERNAME="$(USERNAME)" PASSWORD="$(PASSWORD)" DATABASE_URL="$(DATABASE_URL)" HOST="$(HOST)" PORT=$(PORT) cargo run .PHONY: test test: diff --git a/README.md b/README.md index 635eac4..b1399c7 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ To build the image, use: ### Run -`docker run -e DATABASE_URL=... -e HTTP_SERVER_HOST=... -e HTTP_SERVER_PORT=... kudos-api` +`docker run -e DATABASE_URL=... -e HOST=... -e PORT=... kudos-api` ### Docker-compose diff --git a/docker-compose.yaml b/docker-compose.yaml index b825e15..3de1899 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -11,8 +11,8 @@ services: - "8000:8000" environment: - DATABASE_URL=postgres://postgres:password@db:5432/database - - HTTP_SERVER_HOST=0.0.0.0 - - HTTP_SERVER_PORT=8000 + - HOST=0.0.0.0 + - PORT=8000 depends_on: - db diff --git a/migrations/2024-04-13-204151_init/up.sql b/migrations/2024-04-13-204151_init/up.sql index 18fbfa0..56db71a 100644 --- a/migrations/2024-04-13-204151_init/up.sql +++ b/migrations/2024-04-13-204151_init/up.sql @@ -8,13 +8,15 @@ CREATE TABLE IF NOT EXISTS languages ( id SERIAL PRIMARY KEY, name VARCHAR(255) UNIQUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, - updated_at TIMESTAMP NULL -- used in the issues - CREATE TABLE IF NOT EXISTS labels ( - id SERIAL PRIMARY KEY, - name VARCHAR(255) UNIQUE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, - updated_at TIMESTAMP NULL - ); + updated_at TIMESTAMP NULL +); +-- used in the issues +CREATE TABLE IF NOT EXISTS labels ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) UNIQUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, + updated_at TIMESTAMP NULL +); -- used in the repositories CREATE TABLE IF NOT EXISTS topics ( id SERIAL PRIMARY KEY, @@ -63,9 +65,9 @@ CREATE TABLE IF NOT EXISTS organizations ( -- basic github repository CREATE TABLE IF NOT EXISTS repositories ( id SERIAL PRIMARY KEY, - name VARCHAR(255) NOT NULL, - url VARCHAR(255) NOT NULL, - icon VARCHAR(255) NOT NULL, + name VARCHAR(255), + url VARCHAR(255), + icon VARCHAR(255), e_tag VARCHAR(40) NOT NULL, organization_id INT REFERENCES organizations(id), created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..f8576f1 --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,243 @@ +// @generated automatically by Diesel CLI. + +pub mod sql_types { + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "tip_status"))] + pub struct TipStatus; + + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "tip_type"))] + pub struct TipType; +} + +diesel::table! { + blockchains (id) { + id -> Int4, + #[max_length = 255] + name -> Nullable, + created_at -> Timestamptz, + updated_at -> Nullable, + } +} + +diesel::table! { + comments (id) { + id -> Int4, + wish_id -> Int4, + user_id -> Int4, + #[max_length = 255] + comment -> Nullable, + positive_votes -> Nullable, + negative_votes -> Nullable, + created_at -> Timestamptz, + updated_at -> Nullable, + } +} + +diesel::table! { + filter_values (id) { + id -> Int4, + filters_id -> Int4, + emoji -> Text, + #[max_length = 255] + name -> Nullable, + created_at -> Timestamptz, + updated_at -> Nullable, + } +} + +diesel::table! { + filters (id) { + id -> Int4, + #[max_length = 255] + name -> Nullable, + emoji -> Text, + created_at -> Timestamptz, + updated_at -> Nullable, + } +} + +diesel::table! { + issues (id) { + id -> Int4, + issue_number -> Nullable, + created_at -> Nullable, + updated_at -> Nullable, + repository_id -> Nullable, + } +} + +diesel::table! { + issues_labels (id) { + id -> Int4, + issue_id -> Nullable, + labels_id -> Nullable, + } +} + +diesel::table! { + labels (id) { + id -> Int4, + #[max_length = 255] + name -> Nullable, + created_at -> Timestamptz, + updated_at -> Nullable, + } +} + +diesel::table! { + languages (id) { + id -> Int4, + #[max_length = 255] + name -> Nullable, + created_at -> Timestamptz, + updated_at -> Nullable, + } +} + +diesel::table! { + maintainers (id) { + id -> Int4, + repository_id -> Nullable, + user_id -> Nullable, + } +} + +diesel::table! { + organizations (id) { + id -> Int4, + #[max_length = 255] + name -> Nullable, + created_at -> Nullable, + } +} + +diesel::table! { + repositories (id) { + id -> Int4, + #[max_length = 255] + name -> Nullable, + organization_id -> Nullable, + created_at -> Nullable, + } +} + +diesel::table! { + repositories_filters (id) { + id -> Int4, + repositories_id -> Nullable, + filters_id -> Nullable, + filter_values_id -> Nullable, + } +} + +diesel::table! { + repositories_languages (id) { + id -> Int4, + repositories_id -> Nullable, + languages_id -> Nullable, + } +} + +diesel::table! { + repositories_topics (id) { + id -> Int4, + repositories_id -> Nullable, + topics_id -> Nullable, + } +} + +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::TipStatus; + use super::sql_types::TipType; + + tips (id) { + id -> Int4, + status -> TipStatus, + #[sql_name = "type"] + type_ -> TipType, + amount -> Int8, + #[max_length = 48] + to -> Varchar, + #[max_length = 48] + from -> Varchar, + #[max_length = 255] + transaction -> Nullable, + blockchain_id -> Nullable, + #[max_length = 255] + url -> Nullable, + contributor_id -> Int4, + curator_id -> Nullable, + created_at -> Timestamptz, + updated_at -> Nullable, + } +} + +diesel::table! { + topics (id) { + id -> Int4, + #[max_length = 255] + name -> Nullable, + created_at -> Timestamptz, + updated_at -> Nullable, + } +} + +diesel::table! { + users (id) { + id -> Int4, + #[max_length = 100] + username -> Nullable, + created_at -> Nullable, + } +} + +diesel::table! { + wishes (id) { + id -> Int4, + issues_id -> Int4, + created_at -> Timestamptz, + updated_at -> Nullable, + } +} + +diesel::joinable!(comments -> users (user_id)); +diesel::joinable!(comments -> wishes (wish_id)); +diesel::joinable!(filter_values -> filters (filters_id)); +diesel::joinable!(issues -> repositories (repository_id)); +diesel::joinable!(issues_labels -> issues (issue_id)); +diesel::joinable!(issues_labels -> labels (labels_id)); +diesel::joinable!(maintainers -> repositories (repository_id)); +diesel::joinable!(maintainers -> users (user_id)); +diesel::joinable!(repositories -> organizations (organization_id)); +diesel::joinable!(repositories_filters -> filter_values (filter_values_id)); +diesel::joinable!(repositories_filters -> filters (filters_id)); +diesel::joinable!(repositories_filters -> repositories (repositories_id)); +diesel::joinable!(repositories_languages -> languages (languages_id)); +diesel::joinable!(repositories_languages -> repositories (repositories_id)); +diesel::joinable!(repositories_topics -> repositories (repositories_id)); +diesel::joinable!(repositories_topics -> topics (topics_id)); +diesel::joinable!(tips -> blockchains (blockchain_id)); +diesel::joinable!(wishes -> issues (issues_id)); + +diesel::allow_tables_to_appear_in_same_query!( + blockchains, + comments, + filter_values, + filters, + issues, + issues_labels, + labels, + languages, + maintainers, + organizations, + repositories, + repositories_filters, + repositories_languages, + repositories_topics, + tips, + topics, + users, + wishes, +); diff --git a/src/types.rs b/src/types.rs index 58a0029..b0477f9 100644 --- a/src/types.rs +++ b/src/types.rs @@ -16,12 +16,11 @@ impl ApiConfig { pub fn new() -> Self { dotenv().ok(); Self { - http_server_host: env::var("HTTP_SERVER_HOST") - .unwrap_or_else(|_| "127.0.0.1".to_owned()), - http_server_port: env::var("HTTP_SERVER_PORT") + http_server_host: env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_owned()), + http_server_port: env::var("PORT") .unwrap_or_else(|_| "8000".to_owned()) .parse() - .expect("Invalid HTTP_SERVER_PORT"), + .expect("Invalid PORT"), database_url: env::var("DATABASE_URL").expect("DATABASE_URL must be set"), } } From b69aa6f4fc4efe1342fbc5bdd6fd2c6190ca95ab Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 28 Apr 2024 21:01:47 -0300 Subject: [PATCH 21/98] chore: wake up cron --- .github/workflows/stage_cron.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/workflows/stage_cron.yaml diff --git a/.github/workflows/stage_cron.yaml b/.github/workflows/stage_cron.yaml new file mode 100644 index 0000000..7043943 --- /dev/null +++ b/.github/workflows/stage_cron.yaml @@ -0,0 +1,12 @@ +name: Stage cron +on: + schedule: + - cron: "*/15 * * * *" + +jobs: + stage: + runs-on: ubuntu-latest + steps: + - name: Wake up service + run: | + curl --fail ${{ vars.ISSUES_API_STAGE_URL }}/health From c100204c1a94441a07496810ac81e90c5903f4d5 Mon Sep 17 00:00:00 2001 From: Igor Papandinas <26460174+ipapandinas@users.noreply.github.com> Date: Thu, 2 May 2024 19:52:19 +0200 Subject: [PATCH 22/98] refactor: diesel pool and query builder --- Cargo.lock | 418 +++-------------------------------------- Cargo.toml | 6 +- src/db/errors.rs | 11 +- src/db/pool.rs | 56 +++--- src/db/types.rs | 10 +- src/db/utils.rs | 72 +------ src/error_handler.rs | 39 ++-- src/health/db.rs | 20 +- src/health/handlers.rs | 2 +- src/health/routes.rs | 5 +- src/main.rs | 47 ++--- src/utils.rs | 57 ++++++ 12 files changed, 162 insertions(+), 581 deletions(-) create mode 100644 src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 4f53a00..358b678 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,18 +17,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -53,17 +41,6 @@ dependencies = [ "libc", ] -[[package]] -name = "async-trait" -version = "0.1.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -205,9 +182,11 @@ checksum = "03fc05c17098f21b89bc7d98fe1dd3cce2c11c2ad8e145f2a44fe08ed28eb559" dependencies = [ "bitflags 2.5.0", "byteorder", + "chrono", "diesel_derives", "itoa", "pq-sys", + "r2d2", ] [[package]] @@ -239,7 +218,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", - "subtle", ] [[package]] @@ -263,18 +241,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - -[[package]] -name = "finl_unicode" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" - [[package]] name = "fnv" version = "1.0.7" @@ -290,21 +256,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "futures" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - [[package]] name = "futures-channel" version = "0.3.29" @@ -321,34 +272,6 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" -[[package]] -name = "futures-executor" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" - -[[package]] -name = "futures-macro" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "futures-sink" version = "0.3.29" @@ -361,25 +284,15 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" - [[package]] name = "futures-util" version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ - "futures-channel", "futures-core", - "futures-io", - "futures-macro", "futures-sink", "futures-task", - "memchr", "pin-project-lite", "pin-utils", "slab", @@ -467,15 +380,6 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - [[package]] name = "http" version = "0.2.11" @@ -600,8 +504,6 @@ dependencies = [ "chrono", "diesel", "dotenv", - "mobc", - "mobc-postgres", "regex", "serde", "serde_derive", @@ -612,12 +514,6 @@ dependencies = [ "warp", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" version = "0.2.150" @@ -640,32 +536,12 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - [[package]] name = "memchr" version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" -[[package]] -name = "metrics" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2be3cbd384d4e955b231c895ce10685e3d8260c5ccffae898c96c723b0772835" -dependencies = [ - "ahash", - "portable-atomic", -] - [[package]] name = "mime" version = "0.3.17" @@ -702,36 +578,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "mobc" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8d3681f0b299413df040f53c6950de82e48a8e1a9f79d442ed1ad3694d660b9" -dependencies = [ - "async-trait", - "futures-channel", - "futures-core", - "futures-timer", - "futures-util", - "log", - "metrics", - "thiserror", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "mobc-postgres" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36167a82f9972ccd758596e63c24d63f14e9d1707b41ce9dbfe57c9746b259ce" -dependencies = [ - "futures", - "mobc", - "tokio-postgres", -] - [[package]] name = "multer" version = "2.1.0" @@ -750,16 +596,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - [[package]] name = "num-traits" version = "0.2.17" @@ -794,12 +630,6 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "parking_lot" version = "0.12.1" @@ -829,24 +659,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "phf" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_shared" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" -dependencies = [ - "siphasher", -] - [[package]] name = "pin-project" version = "1.1.3" @@ -879,42 +691,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "portable-atomic" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" - -[[package]] -name = "postgres-protocol" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b6c5ef183cd3ab4ba005f1ca64c21e8bd97ce4699cfea9e8d9a2c4958ca520" -dependencies = [ - "base64 0.21.5", - "byteorder", - "bytes", - "fallible-iterator", - "hmac", - "md-5", - "memchr", - "rand", - "sha2", - "stringprep", -] - -[[package]] -name = "postgres-types" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d2234cdee9408b523530a9b6d2d6b373d1db34f6a8e51dc03ded1828d7fb67c" -dependencies = [ - "bytes", - "chrono", - "fallible-iterator", - "postgres-protocol", -] - [[package]] name = "ppv-lite86" version = "0.2.17" @@ -948,6 +724,17 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + [[package]] name = "rand" version = "0.8.5" @@ -1037,6 +824,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -1103,32 +899,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - [[package]] name = "slab" version = "0.4.9" @@ -1170,23 +940,6 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -[[package]] -name = "stringprep" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" -dependencies = [ - "finl_unicode", - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "subtle" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" - [[package]] name = "syn" version = "2.0.39" @@ -1218,16 +971,6 @@ dependencies = [ "syn", ] -[[package]] -name = "thread_local" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" -dependencies = [ - "cfg-if", - "once_cell", -] - [[package]] name = "tinyvec" version = "1.6.0" @@ -1271,32 +1014,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-postgres" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d340244b32d920260ae7448cb72b6e238bddc3d4f7603394e7dd46ed8e48f5b8" -dependencies = [ - "async-trait", - "byteorder", - "bytes", - "fallible-iterator", - "futures-channel", - "futures-util", - "log", - "parking_lot", - "percent-encoding", - "phf", - "pin-project-lite", - "postgres-protocol", - "postgres-types", - "rand", - "socket2 0.5.5", - "tokio", - "tokio-util", - "whoami", -] - [[package]] name = "tokio-stream" version = "0.1.14" @@ -1348,21 +1065,9 @@ checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "log", "pin-project-lite", - "tracing-attributes", "tracing-core", ] -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tracing-core" version = "0.1.32" @@ -1370,32 +1075,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", ] [[package]] @@ -1476,12 +1155,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - [[package]] name = "vcpkg" version = "0.2.15" @@ -1540,12 +1213,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - [[package]] name = "wasm-bindgen" version = "0.2.89" @@ -1600,27 +1267,6 @@ version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" -[[package]] -name = "web-sys" -version = "0.3.66" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "whoami" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" -dependencies = [ - "redox_syscall", - "wasite", - "web-sys", -] - [[package]] name = "winapi" version = "0.3.9" @@ -1717,23 +1363,3 @@ name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "zerocopy" -version = "0.7.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/Cargo.toml b/Cargo.toml index 878094a..37f8a40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,16 +7,14 @@ edition = "2021" [dependencies] warp = "0.3" -tokio = { version = "1.34", features = ["macros"] } +tokio = { version = "1.34", features = ["macros", "rt-multi-thread"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" dotenv = "0.15" -mobc = "0.8.3" -mobc-postgres = { version = "0.8.0", features = ["with-chrono-0_4"] } thiserror = "1.0.50" chrono = { version = "0.4.31", features = ["serde"] } serde_derive = "1.0.193" base64 = "0.22.0" url = "2.5.0" -diesel = { version = "2.1.5", features = ["postgres"] } +diesel = { version = "2.1.5", features = ["postgres", "chrono", "r2d2"] } regex = "1.10.4" diff --git a/src/db/errors.rs b/src/db/errors.rs index 75a27e5..1160fdb 100644 --- a/src/db/errors.rs +++ b/src/db/errors.rs @@ -1,15 +1,16 @@ -use mobc_postgres::tokio_postgres; +use diesel::r2d2::PoolError; +use diesel::result::Error as DieselError; use thiserror::Error; use tokio::time::error::Elapsed; #[derive(Error, Debug)] pub enum DBError { #[error("error getting connection from DB pool: {0}")] - DBPoolConnection(mobc::Error), + DBPoolConnection(PoolError), #[error("error executing DB query: {0}")] - DBQuery(#[from] tokio_postgres::Error), - #[error("error creating table: {0}")] - DBInit(tokio_postgres::Error), + DBQuery(#[from] DieselError), + #[error("error initializing the database: {0}")] + DBInit(DieselError), #[error("error reading file: {0}")] ReadFile(#[from] std::io::Error), #[error("database operation timed out: {0}")] diff --git a/src/db/pool.rs b/src/db/pool.rs index 64bc691..08a3a4c 100644 --- a/src/db/pool.rs +++ b/src/db/pool.rs @@ -1,55 +1,41 @@ -use mobc::{async_trait, Pool}; -use mobc_postgres::{tokio_postgres, PgConnectionManager}; -use std::fs; -use std::str::FromStr; +use diesel::prelude::*; +use diesel::r2d2::{self, ConnectionManager, PoolError}; +use std::sync::Arc; use std::time::Duration; -use tokio_postgres::{Config, Error, NoTls}; -use crate::db::errors::DBError; -use crate::db::types::{DBCon, DBPool}; +use crate::db::types::{DBConn, DBPool}; -const DB_POOL_MAX_OPEN: u64 = 32; // TODO: move to config -const DB_POOL_MAX_IDLE: u64 = 8; // TODO: move to config +const DB_POOL_MAX_OPEN: u32 = 32; // TODO: move to config +const DB_POOL_MIN_IDLE: u32 = 8; // TODO: move to config const DB_POOL_TIMEOUT_SECONDS: u64 = 15; // TODO: move to config -pub fn create_pool(database_url: &str) -> std::result::Result> { - let config = Config::from_str(database_url)?; - - let manager = PgConnectionManager::new(config, NoTls); - Ok(Pool::builder() - .max_open(DB_POOL_MAX_OPEN) - .max_idle(DB_POOL_MAX_IDLE) - .get_timeout(Some(Duration::from_secs(DB_POOL_TIMEOUT_SECONDS))) - .build(manager)) +pub fn create_db_pool(database_url: &str) -> Result { + let manager = ConnectionManager::::new(database_url); + r2d2::Pool::builder() + .max_size(DB_POOL_MAX_OPEN) + .min_idle(Some(DB_POOL_MIN_IDLE)) + .connection_timeout(Duration::from_secs(DB_POOL_TIMEOUT_SECONDS)) + .build(manager) } -#[async_trait] pub trait DBAccessor: Send + Sync + Clone + 'static { fn new(db_pool: DBPool) -> Self; - async fn get_db_con(&self) -> Result; - async fn init_db(&self, sql_file: &str) -> Result<(), DBError>; + fn get_db_conn(&self) -> DBConn; } #[derive(Clone)] pub struct DBAccess { - pub db_pool: DBPool, + pub db_pool: Arc, } -#[async_trait] + impl DBAccessor for DBAccess { fn new(db_pool: DBPool) -> Self { - Self { db_pool } - } - - async fn get_db_con(&self) -> Result { - self.db_pool.get().await.map_err(DBError::DBPoolConnection) + Self { + db_pool: Arc::new(db_pool), + } } - async fn init_db(&self, sql_file: &str) -> Result<(), DBError> { - let init_file = fs::read_to_string(sql_file)?; - let con = self.get_db_con().await?; - con.batch_execute(init_file.as_str()) - .await - .map_err(DBError::DBInit)?; - Ok(()) + fn get_db_conn(&self) -> DBConn { + self.db_pool.get().expect("Failed to get db connection") } } diff --git a/src/db/types.rs b/src/db/types.rs index 65343fd..1f18739 100644 --- a/src/db/types.rs +++ b/src/db/types.rs @@ -1,6 +1,6 @@ -use mobc::{Connection, Pool}; -use mobc_postgres::{tokio_postgres, PgConnectionManager}; -use tokio_postgres::NoTls; +use diesel::r2d2::PooledConnection; +use diesel::r2d2::{self, ConnectionManager}; +use diesel::PgConnection; -pub type DBCon = Connection>; -pub type DBPool = Pool>; +pub type DBConn = PooledConnection>; +pub type DBPool = r2d2::Pool>; diff --git a/src/db/utils.rs b/src/db/utils.rs index 60b0682..a959a27 100644 --- a/src/db/utils.rs +++ b/src/db/utils.rs @@ -1,79 +1,11 @@ -use super::{errors::DBError, pool::DBAccessor}; -use mobc_postgres::tokio_postgres::{types::ToSql, Row}; +use super::pool::DBAccessor; use regex::Regex; -use tokio::time::{timeout, Duration}; -use warp::reject; +use tokio::time::Duration; pub const DB_QUERY_TIMEOUT: Duration = Duration::from_secs(5); pub const ASC: &str = "ASC"; pub const DESC: &str = "DESC"; -pub async fn query_with_timeout( - db_access: &impl DBAccessor, - query: &str, - params: &[&(dyn ToSql + Sync)], - timeout_duration: Duration, -) -> Result, reject::Rejection> { - let db_conn = db_access.get_db_con().await.map_err(reject::custom)?; - println!("{}", query); - println!("{:#?}", params); - let r = db_conn.query(query, params).await; - if r.is_err() { - let a = r.err().unwrap(); - println!("{}", a); - } - timeout(timeout_duration, db_conn.query(query, params)) - .await - .map_err(|err| reject::custom(DBError::DBTimeout(err)))? - .map_err(|err| reject::custom(DBError::DBQuery(err))) -} - -pub async fn execute_query_with_timeout( - db_access: &impl DBAccessor, - query: &str, - params: &[&(dyn ToSql + Sync)], - timeout_duration: Duration, -) -> Result<(), reject::Rejection> { - let db_conn = db_access.get_db_con().await.map_err(reject::custom)?; - println!("{}", query); - println!("{:#?}", params); - timeout(timeout_duration, db_conn.execute(query, params)) - .await - .map_err(|err| reject::custom(DBError::DBTimeout(err)))? - .map_err(|err| reject::custom(DBError::DBQuery(err)))?; - Ok(()) -} - -pub async fn query_opt_timeout( - db_access: &impl DBAccessor, - query: &str, - params: &[&(dyn ToSql + Sync)], - timeout_duration: Duration, -) -> Result, reject::Rejection> { - let db_conn = db_access.get_db_con().await.map_err(reject::custom)?; - println!("{}", query); - println!("{:#?}", params); - timeout(timeout_duration, db_conn.query_opt(query, params)) - .await - .map_err(|err| reject::custom(DBError::DBTimeout(err)))? - .map_err(|err| reject::custom(DBError::DBQuery(err))) -} - -pub async fn query_one_timeout( - db_access: &impl DBAccessor, - query: &str, - params: &[&(dyn ToSql + Sync)], - timeout_duration: Duration, -) -> Result { - let db_conn = db_access.get_db_con().await.map_err(reject::custom)?; - println!("{}", query); - println!("{:#?}", params); - timeout(timeout_duration, db_conn.query_one(query, params)) - .await - .map_err(|err| reject::custom(DBError::DBTimeout(err)))? - .map_err(|err| reject::custom(DBError::DBQuery(err))) -} - pub fn detect_sql_injection(input: &str) -> bool { let sql_injection_pattern = Regex::new(r"(?i)(\b(?:select|insert|update|delete|drop|alter|create)\b.*\b(?:from|into|table|where)\b|\bunion\b.*\b(?:select|values)\b|\b(?:exec|execute)\b\s*\()").unwrap(); sql_injection_pattern.is_match(input) diff --git a/src/error_handler.rs b/src/error_handler.rs index 7890ab7..f6d243f 100644 --- a/src/error_handler.rs +++ b/src/error_handler.rs @@ -6,10 +6,10 @@ use warp::{hyper::StatusCode, Rejection, Reply}; use crate::{ auth_error::AuthenticationError, db::errors::DBError, - organization::errors::OrganizationError, - pagination::{PaginationError, SortError}, - repository::{errors::RepositoryError, models::RepositorySortError}, - user::{errors::UserError, models::UserSortError}, + // organization::errors::OrganizationError, + // pagination::{PaginationError, SortError}, + // repository::{errors::RepositoryError, models::RepositorySortError}, + // user::{errors::UserError, models::UserSortError}, }; #[derive(Serialize, Deserialize, PartialEq, Debug)] @@ -19,21 +19,22 @@ pub struct ErrorResponse { pub async fn error_handler(err: Rejection) -> std::result::Result { // TODO: improve this - if let Some(e) = err.find::() { - Ok(e.clone().into_response()) - } else if let Some(e) = err.find::() { - Ok(e.clone().into_response()) - } else if let Some(e) = err.find::() { - Ok(e.clone().into_response()) - } else if let Some(e) = err.find::() { - Ok(e.clone().into_response()) - } else if let Some(e) = err.find::() { - Ok(e.clone().into_response()) - } else if let Some(e) = err.find::() { - Ok(e.clone().into_response()) - } else if let Some(e) = err.find::() { - Ok(e.clone().into_response()) - } else if let Some(e) = err.find::() { + // if let Some(e) = err.find::() { + // Ok(e.clone().into_response()) + // } else if let Some(e) = err.find::() { + // Ok(e.clone().into_response()) + // } else if let Some(e) = err.find::() { + // Ok(e.clone().into_response()) + // } else if let Some(e) = err.find::() { + // Ok(e.clone().into_response()) + // } else if let Some(e) = err.find::() { + // Ok(e.clone().into_response()) + // } else if let Some(e) = err.find::() { + // Ok(e.clone().into_response()) + // } else if let Some(e) = err.find::() { + // Ok(e.clone().into_response()) + // } + if let Some(e) = err.find::() { Ok(e.clone().into_response()) } else if let Some(e) = err.find::() { let (code, message) = match e { diff --git a/src/health/db.rs b/src/health/db.rs index 83a192e..593afbc 100644 --- a/src/health/db.rs +++ b/src/health/db.rs @@ -1,20 +1,22 @@ -use mobc::async_trait; -use warp::reject; +use diesel::sql_query; +use diesel::RunQueryDsl; use crate::db::{ - pool::DBAccess, - utils::{execute_query_with_timeout, DB_QUERY_TIMEOUT}, + errors::DBError, + pool::{DBAccess, DBAccessor}, }; -#[async_trait] pub trait DBHealth: Send + Sync + Clone + 'static { - async fn health(&self) -> Result<(), reject::Rejection>; + fn health(&self) -> Result<(), DBError>; } -#[async_trait] impl DBHealth for DBAccess { - async fn health(&self) -> Result<(), reject::Rejection> { - execute_query_with_timeout(self, "SELECT 1", &[], DB_QUERY_TIMEOUT).await?; + fn health(&self) -> Result<(), DBError> { + let conn = &mut self.get_db_conn(); + sql_query("SELECT 1") + .execute(conn) + .map_err(DBError::DBQuery)?; + Ok(()) } } diff --git a/src/health/handlers.rs b/src/health/handlers.rs index b61b86f..d2ab534 100644 --- a/src/health/handlers.rs +++ b/src/health/handlers.rs @@ -3,6 +3,6 @@ use warp::{http::StatusCode, Rejection, Reply}; use super::db::DBHealth; pub async fn health_handler(db_access: impl DBHealth) -> Result { - db_access.health().await?; + db_access.health().map_err(warp::reject::custom)?; Ok(StatusCode::OK) } diff --git a/src/health/routes.rs b/src/health/routes.rs index ec09cbd..dfe8b6a 100644 --- a/src/health/routes.rs +++ b/src/health/routes.rs @@ -7,7 +7,8 @@ use super::handlers; pub fn routes(db_access: impl DBHealth) -> BoxedFilter<(impl Reply,)> { let health_route = warp::path!("health") .and(warp::any().map(move || db_access.clone())) - .and_then(handlers::health_handler); + .and_then(handlers::health_handler) + .boxed(); - health_route.boxed() + health_route } diff --git a/src/main.rs b/src/main.rs index a845d73..eddff8f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,25 +1,20 @@ mod types; -use warp::Filter; - -use crate::{ - db::{ - errors::DBError, - pool::{DBAccess, DBAccessor}, - }, - types::ApiConfig, -}; +use crate::types::ApiConfig; mod auth; mod auth_error; mod db; mod error_handler; mod health; -mod issue; -mod organization; -mod pagination; -mod repository; -mod user; +// mod languages; +// mod issue; +// mod organization; +// mod pagination; +// mod repository; +// mod user; +pub mod schema; +mod utils; #[cfg(test)] mod tests; @@ -36,26 +31,8 @@ async fn run() { database_url, } = ApiConfig::new(); - // init db - let db_pool = db::pool::create_pool(&database_url) - .map_err(DBError::DBPoolConnection) - .expect("Cannot create DB connection"); - let db = DBAccess::new(db_pool); - - let health_route = health::routes::routes(db.clone()); - let users_route = user::routes::routes(db.clone()); - let organizations_route = organization::routes::routes(db.clone()); - let repositories_route = repository::routes::routes(db); - //TODO: add issue route - let error_handler = error_handler::error_handler; - - // string all the routes together - let routes = health_route - .or(users_route) - .or(organizations_route) - .or(repositories_route) - .with(warp::cors().allow_any_origin()) - .recover(error_handler); + let db = utils::setup_db(&database_url).await; + let app_filters = utils::setup_filters(db); let addr = format!("{}:{}", host, port) .parse::() @@ -63,5 +40,5 @@ async fn run() { println!("listening on {}", addr); - warp::serve(routes).run(addr).await; + warp::serve(app_filters).run(addr).await; } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..8b1b6f6 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,57 @@ +use ::warp::Reply; +use warp::{filters::BoxedFilter, Filter}; + +use crate::{ + db::{ + self, + errors::DBError, + pool::{DBAccess, DBAccessor}, + }, + error_handler::error_handler, + health, +}; + +pub async fn setup_db(url: &String) -> DBAccess { + let db_pool = db::pool::create_db_pool(&url) + .map_err(DBError::DBPoolConnection) + .expect("Failed to create DB pool"); + + // TODO: Extend this helper in tests by incorporating DB migration with Diesel + + // // In Cargo.toml + // [dependencies] + // diesel_migrations = "1.4.0" + + // // In main.rs + //#[macro_use] + // extern crate diesel_migrations; + + // embed_migrations!(); + + // // Get a database connection from the pool + // let conn = db_pool.get() + // .expect("Failed to get database connection from pool"); + + // // Run embedded migrations + // embedded_migrations::run(&conn) + // .expect("Failed to run database migrations"); + + DBAccess::new(db_pool) +} + +pub fn setup_filters(db: DBAccess) -> BoxedFilter<(impl Reply,)> { + let health_route = health::routes::routes(db.clone()); + // let repositories_route = repository::routes::routes(db.clone()); + + let error_handler = error_handler; + + health_route + // .or(repositories_route) + .with(warp::cors().allow_any_origin()) + .recover(error_handler) + .boxed() +} + +pub fn parse_ids(s: &str) -> Vec { + s.split(",").map(|id| id.parse::().unwrap()).collect() +} From c7f20dac810acdc9366bb15dc4fe07b534bd5eec Mon Sep 17 00:00:00 2001 From: Igor Papandinas <26460174+ipapandinas@users.noreply.github.com> Date: Sat, 4 May 2024 17:56:37 +0200 Subject: [PATCH 23/98] refactor: reorg and projects endpoint --- migrations/2024-04-13-204151_init/down.sql | 23 +- migrations/2024-04-13-204151_init/up.sql | 178 +------ src/{ => api}/health/db.rs | 0 src/{ => api}/health/handlers.rs | 0 src/{ => api}/health/mod.rs | 0 src/{ => api}/health/routes.rs | 0 src/{ => api}/issue/db.rs | 0 src/{ => api}/issue/errors.rs | 0 src/{ => api}/issue/handlers.rs | 0 src/{ => api}/issue/mod.rs | 0 src/{ => api}/issue/models.rs | 0 src/{ => api}/issue/routes.rs | 0 src/{ => api}/issue/utils.rs | 0 src/api/languages/mod.rs | 1 + src/api/languages/models.rs | 10 + src/api/mod.rs | 6 + src/api/projects/db.rs | 115 ++++ src/api/projects/errors.rs | 51 ++ src/api/projects/handlers.rs | 55 ++ src/{organization => api/projects}/mod.rs | 0 src/api/projects/models.rs | 53 ++ src/api/projects/routes.rs | 54 ++ src/api/repositories/db.rs | 185 +++++++ .../repositories}/errors.rs | 2 +- src/api/repositories/handlers.rs | 100 ++++ src/{repository => api/repositories}/mod.rs | 0 .../repositories}/models.rs | 49 +- src/api/repositories/routes.rs | 65 +++ src/{user => api/users}/db.rs | 0 src/{user => api/users}/errors.rs | 0 src/{user => api/users}/handlers.rs | 0 src/{user => api/users}/mod.rs | 0 src/{user => api/users}/models.rs | 0 src/{user => api/users}/routes.rs | 0 src/{auth_error.rs => auth/errors.rs} | 2 +- src/{auth.rs => auth/mod.rs} | 11 +- src/db/errors.rs | 2 - src/db/mod.rs | 1 - src/db/utils.rs | 24 - src/error_handler.rs | 91 ---- src/errors.rs | 69 +++ src/main.rs | 7 +- src/organization/db.rs | 80 --- src/organization/errors.rs | 46 -- src/organization/handlers.rs | 64 --- src/organization/models.rs | 27 - src/organization/routes.rs | 51 -- src/pagination.rs | 159 ------ src/repository/db.rs | 174 ------- src/repository/handlers.rs | 120 ----- src/repository/routes.rs | 66 --- src/schema.rs | 242 +-------- src/tests/contribution.rs | 493 ------------------ src/tests/error_handler.rs | 0 src/tests/health.rs | 40 +- src/tests/mod.rs | 3 - src/tests/users.rs | 26 +- src/types.rs | 17 + src/utils.rs | 18 +- 59 files changed, 886 insertions(+), 1894 deletions(-) rename src/{ => api}/health/db.rs (100%) rename src/{ => api}/health/handlers.rs (100%) rename src/{ => api}/health/mod.rs (100%) rename src/{ => api}/health/routes.rs (100%) rename src/{ => api}/issue/db.rs (100%) rename src/{ => api}/issue/errors.rs (100%) rename src/{ => api}/issue/handlers.rs (100%) rename src/{ => api}/issue/mod.rs (100%) rename src/{ => api}/issue/models.rs (100%) rename src/{ => api}/issue/routes.rs (100%) rename src/{ => api}/issue/utils.rs (100%) create mode 100644 src/api/languages/mod.rs create mode 100644 src/api/languages/models.rs create mode 100644 src/api/mod.rs create mode 100644 src/api/projects/db.rs create mode 100644 src/api/projects/errors.rs create mode 100644 src/api/projects/handlers.rs rename src/{organization => api/projects}/mod.rs (100%) create mode 100644 src/api/projects/models.rs create mode 100644 src/api/projects/routes.rs create mode 100644 src/api/repositories/db.rs rename src/{repository => api/repositories}/errors.rs (97%) create mode 100644 src/api/repositories/handlers.rs rename src/{repository => api/repositories}/mod.rs (100%) rename src/{repository => api/repositories}/models.rs (71%) create mode 100644 src/api/repositories/routes.rs rename src/{user => api/users}/db.rs (100%) rename src/{user => api/users}/errors.rs (100%) rename src/{user => api/users}/handlers.rs (100%) rename src/{user => api/users}/mod.rs (100%) rename src/{user => api/users}/models.rs (100%) rename src/{user => api/users}/routes.rs (100%) rename src/{auth_error.rs => auth/errors.rs} (96%) rename src/{auth.rs => auth/mod.rs} (91%) delete mode 100644 src/db/utils.rs delete mode 100644 src/error_handler.rs create mode 100644 src/errors.rs delete mode 100644 src/organization/db.rs delete mode 100644 src/organization/errors.rs delete mode 100644 src/organization/handlers.rs delete mode 100644 src/organization/models.rs delete mode 100644 src/organization/routes.rs delete mode 100644 src/pagination.rs delete mode 100644 src/repository/db.rs delete mode 100644 src/repository/handlers.rs delete mode 100644 src/repository/routes.rs delete mode 100644 src/tests/contribution.rs delete mode 100644 src/tests/error_handler.rs diff --git a/migrations/2024-04-13-204151_init/down.sql b/migrations/2024-04-13-204151_init/down.sql index c988c3e..e9b1a24 100644 --- a/migrations/2024-04-13-204151_init/down.sql +++ b/migrations/2024-04-13-204151_init/down.sql @@ -1,23 +1,2 @@ -- Drop tables -DROP TABLE IF EXISTS comments; -DROP TABLE IF EXISTS wishes; -DROP TABLE IF EXISTS maintainers; -DROP TABLE IF EXISTS issues_labels; -DROP TABLE IF EXISTS repositories_filters; -DROP TABLE IF EXISTS repositories_topics; -DROP TABLE IF EXISTS repositories_languages; -DROP TABLE IF EXISTS issues; -DROP TABLE IF EXISTS tips; -DROP TABLE IF EXISTS repositories; -DROP TABLE IF EXISTS organizations; -DROP TABLE IF EXISTS users; -DROP TABLE IF EXISTS filter_values; -DROP TABLE IF EXISTS filters; -DROP TABLE IF EXISTS blockchains; -DROP TABLE IF EXISTS topics; -DROP TABLE IF EXISTS labels; -DROP TABLE IF EXISTS languages; --- Drop enums -DROP TYPE IF EXISTS tip_status; -DROP TYPE IF EXISTS issue_status; -DROP TYPE IF EXISTS tip_type; \ No newline at end of file +DROP TABLE IF EXISTS projects; \ No newline at end of file diff --git a/migrations/2024-04-13-204151_init/up.sql b/migrations/2024-04-13-204151_init/up.sql index 56db71a..b9bd02f 100644 --- a/migrations/2024-04-13-204151_init/up.sql +++ b/migrations/2024-04-13-204151_init/up.sql @@ -1,174 +1,12 @@ --- some enums -CREATE TYPE tip_status AS ENUM ('set', 'paid', 'rejected'); -CREATE TYPE issue_status AS ENUM ('open', 'closed'); -CREATE TYPE tip_type AS ENUM ('direct', 'gov'); --- some tables to save metadata associated to the issues, repositories and tips --- used in the repositories -CREATE TABLE IF NOT EXISTS languages ( - id SERIAL PRIMARY KEY, - name VARCHAR(255) UNIQUE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, - updated_at TIMESTAMP NULL -); --- used in the issues -CREATE TABLE IF NOT EXISTS labels ( - id SERIAL PRIMARY KEY, - name VARCHAR(255) UNIQUE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, - updated_at TIMESTAMP NULL -); --- used in the repositories -CREATE TABLE IF NOT EXISTS topics ( - id SERIAL PRIMARY KEY, - name VARCHAR(255) UNIQUE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, - updated_at TIMESTAMP NULL -); --- used in the tips -CREATE TABLE IF NOT EXISTS blockchains ( - id SERIAL PRIMARY KEY, - name VARCHAR(255) UNIQUE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, - updated_at TIMESTAMP NULL -); --- filters used by the frontend: languages, interests, etc -CREATE TABLE IF NOT EXISTS filters ( - id SERIAL PRIMARY KEY, - name VARCHAR(255) UNIQUE, - emoji TEXT NOT NULL, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, - updated_at TIMESTAMP NULL -); --- filters used by the frontend: EVM, documentation, etc. -CREATE TABLE IF NOT EXISTS filter_values ( - id SERIAL PRIMARY KEY, - filters_id INT REFERENCES filters(id) NOT NULL, - emoji TEXT NOT NULL, - name VARCHAR(255) UNIQUE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, - updated_at TIMESTAMP NULL -); --- basic github organization -CREATE TABLE IF NOT EXISTS organizations ( - id SERIAL PRIMARY KEY, - name VARCHAR(255) UNIQUE, - icon VARCHAR(255), - description VARCHAR(255), - url VARCHAR(255), - email VARCHAR(255), - twitter VARCHAR(255), - e_tag VARCHAR(40) NOT NULL, - is_verified boolean, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, - updated_at TIMESTAMP NULL -); -- basic github repository -CREATE TABLE IF NOT EXISTS repositories ( - id SERIAL PRIMARY KEY, - name VARCHAR(255), - url VARCHAR(255), - icon VARCHAR(255), - e_tag VARCHAR(40) NOT NULL, - organization_id INT REFERENCES organizations(id), - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, - updated_at TIMESTAMP NULL -); --- all the users including maintainers and admins -CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, - username VARCHAR(100) UNIQUE NOT NULL, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL -); --- two types of tips: gov and direct. One tip per issue -CREATE TABLE IF NOT EXISTS tips ( - id SERIAL PRIMARY KEY, - status tip_status NOT NULL, - type tip_type NOT NULL, - amount BIGINT CHECK (amount >= 0) NOT NULL, - "to" VARCHAR(48) NOT NULL, - "from" VARCHAR(48) NOT NULL, - --direct - transaction VARCHAR(255) NULL, - blockchain_id INT REFERENCES blockchains(id) NULL, - -- gov - url VARCHAR(255) NULL, - contributor_id INT REFERENCES users(id) NOT NULL, - --not needed if it's a proposal - curator_id INT REFERENCES users(id) NULL, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, - updated_at TIMESTAMP NULL -); --- issue with repository, user and tip -CREATE TABLE IF NOT EXISTS issues ( - id SERIAL PRIMARY KEY, - issue_number INT NOT NULL, - url VARCHAR(255), - title VARCHAR(255), - description VARCHAR(255), - status issue_status NOT NULL, - has_wishes boolean, - repository_id INT REFERENCES repositories(id) NOT NULL, - user_id INT REFERENCES users(id) NULL, - tip_id INT REFERENCES tips(id) NULL, - issue_created_at TIMESTAMP NOT NULL, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, - e_tag VARCHAR(40) NOT NULL, - updated_at TIMESTAMP NULL -); --- one repository can have multiple languages --- one language can have multiple repositories -CREATE TABLE IF NOT EXISTS repositories_languages ( - id SERIAL PRIMARY KEY, - repositories_id INT REFERENCES repositories(id), - languages_id INT REFERENCES languages(id) -); --- one repository can have multiple topics --- one topic can have multiple repositories -CREATE TABLE IF NOT EXISTS repositories_topics ( - id SERIAL PRIMARY KEY, - repositories_id INT REFERENCES repositories(id), - topics_id INT REFERENCES topics(id) -); --- we need to associate the repository (and then the issue) to some --- filters that are used by the frontend. --- For example, "Interests" -> "EVM" and "Network" -> "Kusama", "Polkadot" --- one repository can have multiple filters --- one topic can have multiple repositories -CREATE TABLE IF NOT EXISTS repositories_filters ( +CREATE TABLE IF NOT EXISTS projects ( id SERIAL PRIMARY KEY, - repositories_id INT REFERENCES repositories(id), - filters_id INT REFERENCES filters(id), - filter_values_id INT REFERENCES filter_values(id) -); --- one issue can have multiple labels --- one label can have multiple issues -CREATE TABLE IF NOT EXISTS issues_labels ( - id SERIAL PRIMARY KEY, - issue_id INT REFERENCES issues(id), - labels_id INT REFERENCES labels(id) -); --- one user can be maintainer in multiple repositories --- one repository can have multiple maintainers -CREATE TABLE IF NOT EXISTS maintainers ( - id SERIAL PRIMARY KEY, - repository_id INT REFERENCES repositories(id), - user_id INT REFERENCES users(id) -); --- wishes are special issues where comments are fetched -CREATE TABLE IF NOT EXISTS wishes ( - id SERIAL PRIMARY KEY, - issues_id INT REFERENCES issues(id) NOT NULL, + name VARCHAR(255) NOT NULL, + slug VARCHAR(255) NOT NULL UNIQUE, + categories TEXT[], + purposes TEXT[], + stack_levels TEXT[], + technologies TEXT[], created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, - updated_at TIMESTAMP NULL + updated_at TIMESTAMP WITH TIME ZONE NULL ); --- each wish has multiple comments -CREATE TABLE IF NOT EXISTS comments ( - id SERIAL PRIMARY KEY, - wish_id INT REFERENCES wishes(id) NOT NULL, - user_id INT REFERENCES users(id) NOT NULL, - comment VARCHAR(255) UNIQUE, - positive_votes INT, - negative_votes INT, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, - updated_at TIMESTAMP NULL -); \ No newline at end of file diff --git a/src/health/db.rs b/src/api/health/db.rs similarity index 100% rename from src/health/db.rs rename to src/api/health/db.rs diff --git a/src/health/handlers.rs b/src/api/health/handlers.rs similarity index 100% rename from src/health/handlers.rs rename to src/api/health/handlers.rs diff --git a/src/health/mod.rs b/src/api/health/mod.rs similarity index 100% rename from src/health/mod.rs rename to src/api/health/mod.rs diff --git a/src/health/routes.rs b/src/api/health/routes.rs similarity index 100% rename from src/health/routes.rs rename to src/api/health/routes.rs diff --git a/src/issue/db.rs b/src/api/issue/db.rs similarity index 100% rename from src/issue/db.rs rename to src/api/issue/db.rs diff --git a/src/issue/errors.rs b/src/api/issue/errors.rs similarity index 100% rename from src/issue/errors.rs rename to src/api/issue/errors.rs diff --git a/src/issue/handlers.rs b/src/api/issue/handlers.rs similarity index 100% rename from src/issue/handlers.rs rename to src/api/issue/handlers.rs diff --git a/src/issue/mod.rs b/src/api/issue/mod.rs similarity index 100% rename from src/issue/mod.rs rename to src/api/issue/mod.rs diff --git a/src/issue/models.rs b/src/api/issue/models.rs similarity index 100% rename from src/issue/models.rs rename to src/api/issue/models.rs diff --git a/src/issue/routes.rs b/src/api/issue/routes.rs similarity index 100% rename from src/issue/routes.rs rename to src/api/issue/routes.rs diff --git a/src/issue/utils.rs b/src/api/issue/utils.rs similarity index 100% rename from src/issue/utils.rs rename to src/api/issue/utils.rs diff --git a/src/api/languages/mod.rs b/src/api/languages/mod.rs new file mode 100644 index 0000000..c446ac8 --- /dev/null +++ b/src/api/languages/mod.rs @@ -0,0 +1 @@ +pub mod models; diff --git a/src/api/languages/models.rs b/src/api/languages/models.rs new file mode 100644 index 0000000..5016b20 --- /dev/null +++ b/src/api/languages/models.rs @@ -0,0 +1,10 @@ +use crate::schema::languages; +use diesel::prelude::*; + +#[derive(Queryable, Identifiable, Selectable, Debug, PartialEq)] +#[diesel(table_name = languages)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Language { + pub id: i32, + pub name: String, +} diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..6d634d9 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,6 @@ +pub mod health; +// pub mod issue; +// pub mod languages; +pub mod projects; +// pub mod repositories; +// pub mod users; diff --git a/src/api/projects/db.rs b/src/api/projects/db.rs new file mode 100644 index 0000000..6e09950 --- /dev/null +++ b/src/api/projects/db.rs @@ -0,0 +1,115 @@ +use diesel::dsl::now; +use diesel::prelude::*; + +use super::models::{NewForm, Project, QueryParams, UpdateForm}; +use crate::schema::projects::dsl as projects_dsl; + +use crate::db::{ + errors::DBError, + pool::{DBAccess, DBAccessor}, +}; +use crate::types::PaginationParams; +use crate::utils; + +pub trait DBProject: Send + Sync + Clone + 'static { + fn all( + &self, + params: QueryParams, + pagination: PaginationParams, + ) -> Result, DBError>; + fn by_id(&self, id: i32) -> Result, DBError>; + fn by_slug(&self, slug: &str) -> Result, DBError>; + fn create(&self, form: &NewForm) -> Result; + fn update(&self, id: i32, form: &UpdateForm) -> Result; + fn delete(&self, id: i32) -> Result<(), DBError>; +} + +impl DBProject for DBAccess { + fn all( + &self, + params: QueryParams, + pagination: PaginationParams, + ) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + let mut query = projects_dsl::projects.into_boxed(); + + if let Some(slug) = params.slug { + query = query.filter(projects_dsl::slug.eq(slug)); + } + + if let Some(raw_categories) = params.categories { + let categories: Vec = utils::parse_comma_values(&raw_categories); + query = query.filter(projects_dsl::categories.overlaps_with(categories)); + } + + if let Some(raw_purposes) = params.purposes { + let purposes: Vec = utils::parse_comma_values(&raw_purposes); + query = query.filter(projects_dsl::purposes.overlaps_with(purposes)); + } + + if let Some(raw_technologies) = params.technologies { + let technologies: Vec = utils::parse_comma_values(&raw_technologies); + query = query.filter(projects_dsl::technologies.overlaps_with(technologies)); + } + + query = query.offset(pagination.offset).limit(pagination.limit); + + let result = query.load::(conn)?; + Ok(result) + } + + fn by_id(&self, id: i32) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + + let result = projects_dsl::projects + .find(id) + .first::(conn) + .optional() + .map_err(DBError::from)?; + + Ok(result) + } + + fn by_slug(&self, slug: &str) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + + let result = projects_dsl::projects + .filter(projects_dsl::slug.eq(slug)) + .first::(conn) + .optional() + .map_err(DBError::from)?; + + Ok(result) + } + + fn create(&self, form: &NewForm) -> Result { + let conn = &mut self.get_db_conn(); + + let project = diesel::insert_into(projects_dsl::projects) + .values(form) + .get_result(conn) + .map_err(DBError::from)?; + + Ok(project) + } + + fn update(&self, id: i32, form: &UpdateForm) -> Result { + let conn = &mut self.get_db_conn(); + + let project = diesel::update(projects_dsl::projects.filter(projects_dsl::id.eq(id))) + .set((form, projects_dsl::updated_at.eq(now))) + .get_result::(conn) + .map_err(DBError::from)?; + + Ok(project) + } + + fn delete(&self, id: i32) -> Result<(), DBError> { + let conn = &mut self.get_db_conn(); + diesel::delete(projects_dsl::projects.filter(projects_dsl::id.eq(id))) + .execute(conn) + .map_err(DBError::from)?; + + Ok(()) + } +} diff --git a/src/api/projects/errors.rs b/src/api/projects/errors.rs new file mode 100644 index 0000000..18c378b --- /dev/null +++ b/src/api/projects/errors.rs @@ -0,0 +1,51 @@ +use std::fmt; + +use serde_derive::Deserialize; +use thiserror::Error; +use warp::{ + http::StatusCode, + reject::Reject, + reply::{Reply, Response}, +}; + +use crate::errors::ErrorResponse; + +#[derive(Clone, Error, Debug, Deserialize, PartialEq)] +pub enum ProjectError { + AlreadyExists(i32), + NotFound(i32), + NotFoundBySlug(String), +} + +impl fmt::Display for ProjectError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ProjectError::AlreadyExists(id) => { + write!(f, "Project #{id} already exists") + } + ProjectError::NotFound(id) => { + write!(f, "Project #{id} not found") + } + ProjectError::NotFoundBySlug(slug) => { + write!(f, "Project {slug} not found") + } + } + } +} + +impl Reject for ProjectError {} + +impl Reply for ProjectError { + fn into_response(self) -> Response { + let code = match self { + ProjectError::AlreadyExists(_) => StatusCode::BAD_REQUEST, + ProjectError::NotFound(_) => StatusCode::NOT_FOUND, + ProjectError::NotFoundBySlug(_) => StatusCode::NOT_FOUND, + }; + let message = self.to_string(); + + let json = warp::reply::json(&ErrorResponse { message }); + + warp::reply::with_status(json, code).into_response() + } +} diff --git a/src/api/projects/handlers.rs b/src/api/projects/handlers.rs new file mode 100644 index 0000000..b2aa7ba --- /dev/null +++ b/src/api/projects/handlers.rs @@ -0,0 +1,55 @@ +use crate::types::PaginationParams; + +use super::{ + db::DBProject, + errors::ProjectError, + models::{NewForm, QueryParams, UpdateForm}, +}; +use warp::{ + http::StatusCode, + reject::Rejection, + reply::{json, with_status, Reply}, +}; + +pub async fn all_handler( + db_access: impl DBProject, + params: QueryParams, + pagination: PaginationParams, +) -> Result { + let projects = db_access.all(params, pagination)?; + Ok(json::>(&projects)) +} + +pub async fn create_handler( + form: NewForm, + db_access: impl DBProject, +) -> Result { + match db_access.by_slug(&form.slug)? { + Some(p) => Err(warp::reject::custom(ProjectError::AlreadyExists(p.id))), + None => Ok(with_status( + json(&db_access.create(&form)?), + StatusCode::CREATED, + )), + } +} + +pub async fn update_handler( + id: i32, + form: UpdateForm, + db_access: impl DBProject, +) -> Result { + match db_access.by_id(id)? { + Some(p) => Ok(with_status( + json(&db_access.update(p.id, &form)?), + StatusCode::OK, + )), + None => Err(warp::reject::custom(ProjectError::NotFound(id))), + } +} + +pub async fn delete_handler(id: i32, db_access: impl DBProject) -> Result { + match db_access.by_id(id)? { + Some(p) => Ok(with_status(json(&db_access.delete(p.id)?), StatusCode::OK)), + None => Err(warp::reject::custom(ProjectError::NotFound(id))), + } +} diff --git a/src/organization/mod.rs b/src/api/projects/mod.rs similarity index 100% rename from src/organization/mod.rs rename to src/api/projects/mod.rs diff --git a/src/api/projects/models.rs b/src/api/projects/models.rs new file mode 100644 index 0000000..954fcfc --- /dev/null +++ b/src/api/projects/models.rs @@ -0,0 +1,53 @@ +use crate::schema::projects; +use chrono::{DateTime, Utc}; +use diesel::prelude::*; + +use serde_derive::{Deserialize, Serialize}; + +#[derive( + AsChangeset, Queryable, Identifiable, Selectable, Debug, PartialEq, Serialize, Deserialize, +)] +#[diesel(table_name = projects)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Project { + pub id: i32, + pub name: String, + pub slug: String, + pub categories: Option>>, + pub purposes: Option>>, + pub stack_levels: Option>>, + pub technologies: Option>>, + pub created_at: DateTime, + pub updated_at: Option>, +} + +#[derive(Deserialize, Debug)] +pub struct QueryParams { + pub slug: Option, + pub categories: Option, + pub purposes: Option, + pub stack_levels: Option, + pub technologies: Option, +} + +#[derive(Insertable, Serialize, Deserialize, Debug)] +#[diesel(table_name = projects)] +pub struct NewForm { + pub name: String, + pub slug: String, + pub categories: Option>>, + pub purposes: Option>>, + pub stack_levels: Option>>, + pub technologies: Option>>, +} + +#[derive(AsChangeset, Serialize, Deserialize, Debug)] +#[diesel(table_name = projects)] +pub struct UpdateForm { + pub name: Option, + pub slug: Option, + pub categories: Option>>, + pub purposes: Option>>, + pub stack_levels: Option>>, + pub technologies: Option>>, +} diff --git a/src/api/projects/routes.rs b/src/api/projects/routes.rs new file mode 100644 index 0000000..199e672 --- /dev/null +++ b/src/api/projects/routes.rs @@ -0,0 +1,54 @@ +use std::convert::Infallible; +use warp::filters::BoxedFilter; +use warp::{Filter, Reply}; + +use crate::auth::with_auth; +use crate::types::PaginationParams; + +use super::db::DBProject; +use super::handlers; +use super::models::QueryParams; + +fn with_db( + db_pool: impl DBProject, +) -> impl Filter + Clone { + warp::any().map(move || db_pool.clone()) +} + +pub fn routes(db_access: impl DBProject) -> BoxedFilter<(impl Reply,)> { + let project = warp::path!("projects"); + let project_id = warp::path!("projects" / i32); + + let all_route = project + .and(warp::get()) + .and(with_db(db_access.clone())) + .and(warp::query::()) + .and(warp::query::()) + .and_then(handlers::all_handler); + + let create_route = project + .and(with_auth()) + .and(warp::post()) + .and(warp::body::json()) + .and(with_db(db_access.clone())) + .and_then(handlers::create_handler); + + let update_route = project_id + .and(with_auth()) + .and(warp::put()) + .and(warp::body::json()) + .and(with_db(db_access.clone())) + .and_then(handlers::update_handler); + + let delete_route = project_id + .and(with_auth()) + .and(warp::delete()) + .and(with_db(db_access.clone())) + .and_then(handlers::delete_handler); + + all_route + .or(create_route) + .or(update_route) + .or(delete_route) + .boxed() +} diff --git a/src/api/repositories/db.rs b/src/api/repositories/db.rs new file mode 100644 index 0000000..4d68f41 --- /dev/null +++ b/src/api/repositories/db.rs @@ -0,0 +1,185 @@ +use diesel::{associations::HasTable, prelude::*}; +use warp::reject; + +use super::models::{NewRepository, RepositoriesRelations, Repository, RepositoryQueryParams}; +use crate::schema::languages::dsl::*; +use crate::schema::repositories::dsl as repositories_dsl; +use crate::schema::repositories::dsl::*; +use crate::utils; +use crate::{ + db::{ + errors::DBError, + pool::{DBAccess, DBAccessor}, + }, + languages::models::Language, +}; +// use crate::pagination::GetPagination; + +const TABLE: &str = "repositories"; + +pub trait DBRepository: Send + Sync + Clone + 'static { + fn get_repository(&self, id: i32) -> Result, reject::Rejection>; + fn get_repositories( + &self, + params: RepositoryQueryParams, + ) -> Result, reject::Rejection>; + // async fn get_repository_by_name( + // &self, + // name: &str, + // relations: RepositoriesRelations, + // ) -> Result, reject::Rejection>; + // async fn get_repositories( + // &self, + // relations: RepositoriesRelations, + // pagination: GetPagination, + // sort: RepositorySort, + // ) -> Result, reject::Rejection>; + // async fn create_repository( + // &self, + // repository: NewRepository, + // ) -> Result; + // async fn delete_repository(&self, id: i32) -> Result<(), reject::Rejection>; +} + +impl DBRepository for DBAccess { + // async fn get_repository( + // &self, + // id: i32, + // relations: RepositoriesRelations, + // ) -> Result, reject::Rejection> { + // let mut query = format!("SELECT * FROM {} ", TABLE); + // if relations.issues { + // query += "LEFT JOIN issues on issues.repository_id = repositories.id "; + // if relations.tips { + // query += "LEFT JOIN tips on tips.id = issues.id "; + // } + // } + // if relations.maintainers { + // query += "LEFT JOIN maintainers on maintainers.repository_id = repositories.id "; + // query += "LEFT JOIN users on maintainers.user_id = users.id "; + // } + // if relations.languages { + // query += "LEFT JOIN languages on repositories.languages_id = languages.id "; + // } + // query += "WHERE id = $1"; + + // match query_opt_timeout(self, query.as_str(), &[&id], DB_QUERY_TIMEOUT).await? { + // Some(repository) => Ok(Some(row_to_repository(&repository))), + // None => Ok(None), + // } + // } + + fn get_repositories( + &self, + params: RepositoryQueryParams, + ) -> Result, reject::Rejection> { + let conn = &mut self.get_db_conn(); + let mut query = repositories_dsl::repositories.into_boxed(); + + if let Some(language_ids) = params.language { + let ids: Vec = utils::parse_ids(&language_ids); // TODO: handle parsing error + query = query.filter(repositories_dsl::language_id.eq_any(ids)); + } + + query + .load::(conn) + .map_err(|e| reject::custom(DBError::DBQuery(e))) + } + + fn get_repository(&self, repo_id: i32) -> Result, reject::Rejection> { + let conn = &mut self.get_db_conn(); + repositories + .find(repo_id) + .first::(conn) + .optional() + .map_err(|e| reject::custom(DBError::DBQuery(e))) + } + + // async fn get_repository_by_name( + // &self, + // name: &str, + // relations: RepositoriesRelations, + // ) -> Result, reject::Rejection> { + // let mut query = format!("SELECT * FROM {} ", TABLE); + // if relations.issues { + // query += "LEFT JOIN issues on issues.repository_id = repositories.id "; + // if relations.tips { + // query += "LEFT JOIN tips on tips.id = issues.id "; + // } + // } + // if relations.maintainers { + // query += "LEFT JOIN maintainers on maintainers.repository_id = repositories.id "; + // query += "LEFT JOIN users on maintainers.user_id = users.id "; + // } + // if relations.languages { + // query += "LEFT JOIN languages on repositories.languages_id = languages.id "; + // } + // query += "WHERE name = $1"; + // match query_opt_timeout(self, query.as_str(), &[&name], DB_QUERY_TIMEOUT).await? { + // Some(repository) => Ok(Some(row_to_repository(&repository))), + // None => Ok(None), + // } + // } + + // async fn get_repositories( + // &self, + // relations: RepositoriesRelations, + // pagination: GetPagination, + // sort: RepositorySort, + // ) -> Result, reject::Rejection> { + // let mut query = format!("SELECT * FROM {} ", TABLE); + // if relations.issues { + // query += "LEFT JOIN issues on issues.repository_id = repositories.id "; + // if relations.tips { + // query += "LEFT JOIN tips on tips.id = issues.id "; + // } + // } + // if relations.maintainers { + // query += "LEFT JOIN maintainers on maintainers.repository_id = repositories.id "; + // query += "LEFT JOIN users on maintainers.user_id = users.id "; + // } + // if relations.languages { + // query += "LEFT JOIN languages on repositories.languages_id = languages.id "; + // } + // query += &format!("ORDER BY {} {}", sort.field, sort.order); // cannot use $1 or $2 + + // query += "LIMIT $1 OFFSET $2"; + // let rows = query_with_timeout( + // self, + // query.as_str(), + // &[&pagination.limit, &pagination.offset], + // DB_QUERY_TIMEOUT, + // ) + // .await?; + // Ok(rows.iter().map(row_to_repository).collect()) + // } + + // async fn create_repository( + // &self, + // repository: NewRepository, + // ) -> Result { + // let query = format!( + // "INSERT INTO {} (name, icon, organization_id, url, e_tag) VALUES ($1, $2, $3, $4, $5) RETURNING *", + // TABLE + // ); + // let row = query_one_timeout( + // self, + // &query, + // &[ + // &repository.name, + // &repository.icon, + // &repository.organization_id, + // &repository.url, + // &repository.e_tag, + // ], + // DB_QUERY_TIMEOUT, + // ) + // .await?; + // Ok(row_to_repository(&row)) + // } + + // async fn delete_repository(&self, id: i32) -> Result<(), reject::Rejection> { + // let query = format!("DELETE FROM {} WHERE id = $1", TABLE); + // execute_query_with_timeout(self, &query, &[&id], DB_QUERY_TIMEOUT).await + // } +} diff --git a/src/repository/errors.rs b/src/api/repositories/errors.rs similarity index 97% rename from src/repository/errors.rs rename to src/api/repositories/errors.rs index 9d1ac10..329966e 100644 --- a/src/repository/errors.rs +++ b/src/api/repositories/errors.rs @@ -8,7 +8,7 @@ use warp::{ reply::{Reply, Response}, }; -use crate::error_handler::ErrorResponse; +use crate::errors::ErrorResponse; #[derive(Clone, Error, Debug, Deserialize, PartialEq)] pub enum RepositoryError { diff --git a/src/api/repositories/handlers.rs b/src/api/repositories/handlers.rs new file mode 100644 index 0000000..8b863fc --- /dev/null +++ b/src/api/repositories/handlers.rs @@ -0,0 +1,100 @@ +use warp::{ + http::StatusCode, + reject::Rejection, + reply::{json, with_status, Reply}, +}; + +// use crate::{ +// organization::{db::DBOrganization, errors::OrganizationError}, +// pagination::GetSort, +// }; + +use super::{ + db::DBRepository, + errors::RepositoryError, + models::{ + GetRepositoryQuery, NewRepository, RepositoriesRelations, RepositoryQueryParams, + RepositoryResponse, + }, +}; +// use crate::pagination::GetPagination; +use crate::repository::models::RepositorySort; + +// pub async fn create_repository_handler( +// body: NewRepository, +// db_access: impl DBRepository + DBOrganization, +// ) -> Result { +// match db_access.get_organization(body.organization_id).await? { +// Some(_) => match db_access +// .get_repository_by_name(&body.name, RepositoriesRelations::default()) +// .await? +// { +// Some(u) => Err(warp::reject::custom(RepositoryError::AlreadyExists(u.id)))?, +// None => Ok(with_status( +// json(&RepositoryResponse::of( +// db_access.create_repository(body).await?, +// )), +// StatusCode::CREATED, +// )), +// }, +// None => Err(warp::reject::custom( +// OrganizationError::OrganizationNotFound(body.organization_id), +// ))?, +// } +// } + +pub async fn get_repository_handler( + id: i32, + db_access: impl DBRepository, +) -> Result { + match db_access.get_repository(id)? { + None => Err(warp::reject::custom(RepositoryError::NotFound(id)))?, + Some(repository) => Ok(json(&RepositoryResponse::of(repository))), + } +} + +// pub async fn get_repository_handler_name( +// name: String, +// db_access: impl DBRepository, +// query: GetRepositoryQuery, +// ) -> Result { +// let relations = RepositoriesRelations { +// tips: query.tips.unwrap_or_default(), +// maintainers: query.maintainers.unwrap_or_default(), +// issues: query.issues.unwrap_or_default(), +// languages: query.languages.unwrap_or_default(), +// }; +// match db_access.get_repository_by_name(&name, relations).await? { +// None => Err(warp::reject::custom(RepositoryError::NotFoundByName(name)))?, +// Some(repository) => Ok(json(&RepositoryResponse::of(repository))), +// } +// } + +pub async fn get_repositories_handler( + db_access: impl DBRepository, + params: RepositoryQueryParams, +) -> Result { + let repositories = db_access.get_repositories(params)?; + Ok(json::>( + &repositories + .into_iter() + .map(RepositoryResponse::of) + .collect(), + )) +} + +// pub async fn delete_repository_handler( +// id: i32, +// db_access: impl DBRepository, +// ) -> Result { +// match db_access +// .get_repository(id, RepositoriesRelations::default()) +// .await? +// { +// Some(_) => { +// let _ = &db_access.delete_repository(id).await?; +// Ok(StatusCode::NO_CONTENT) +// } +// None => Err(warp::reject::custom(RepositoryError::NotFound(id)))?, +// } +// } diff --git a/src/repository/mod.rs b/src/api/repositories/mod.rs similarity index 100% rename from src/repository/mod.rs rename to src/api/repositories/mod.rs diff --git a/src/repository/models.rs b/src/api/repositories/models.rs similarity index 71% rename from src/repository/models.rs rename to src/api/repositories/models.rs index 3355b7c..5cd42bb 100644 --- a/src/repository/models.rs +++ b/src/api/repositories/models.rs @@ -1,5 +1,7 @@ use std::fmt; +use chrono::{DateTime, Utc}; +use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable}; use serde_derive::{Deserialize, Serialize}; use warp::{ http::StatusCode, @@ -7,22 +9,37 @@ use warp::{ reply::{Reply, Response}, }; +use crate::languages::models::Language; +use crate::schema::repositories; +use diesel::prelude::*; + use crate::{ db::utils::{default_sort_direction, sort_direction}, error_handler::ErrorResponse, }; use thiserror::Error; -#[derive(Deserialize)] +#[derive(Queryable, Selectable, Identifiable, Associations, Debug, PartialEq)] +#[diesel(belongs_to(Language))] +#[diesel(table_name = repositories)] +#[diesel(check_for_backend(diesel::pg::Pg))] pub struct Repository { pub id: i32, pub name: String, - pub organization_id: i32, - pub icon: String, - pub url: String, - pub e_tag: String, + pub language_id: i32, + // pub url: String, + // pub icon: Option, + // pub e_tag: String, + // pub organization_id: Option, + // pub created_at: DateTime, + // pub updated_at: Option>, } -#[derive(Serialize, Deserialize)] + +#[derive(Deserialize)] +pub struct RepositoryQueryParams { + pub language: Option, +} + pub struct NewRepository { pub name: String, pub icon: String, @@ -35,10 +52,12 @@ pub struct NewRepository { pub struct RepositoryResponse { pub id: i32, pub name: String, - pub organization_id: i32, - pub icon: String, - pub url: String, - pub e_tag: String, + // pub url: String, + // pub icon: Option, + // pub e_tag: String, + // pub organization_id: Option, + // pub created_at: DateTime, + // pub updated_at: Option>, } impl RepositoryResponse { @@ -46,10 +65,12 @@ impl RepositoryResponse { RepositoryResponse { id: repository.id, name: repository.name, - organization_id: repository.organization_id, - icon: repository.icon, - url: repository.url, - e_tag: repository.e_tag, + // organization_id: repository.organization_id, + // icon: repository.icon, + // url: repository.url, + // e_tag: repository.e_tag, + // created_at: repository.created_at, + // updated_at: repository.updated_at, } } } diff --git a/src/api/repositories/routes.rs b/src/api/repositories/routes.rs new file mode 100644 index 0000000..0983fb1 --- /dev/null +++ b/src/api/repositories/routes.rs @@ -0,0 +1,65 @@ +use std::convert::Infallible; + +use warp::filters::BoxedFilter; +use warp::{Filter, Reply}; + +use crate::auth::with_auth; +use crate::organization::db::DBOrganization; + +use super::db::DBRepository; +use super::handlers; +use super::models::RepositoryQueryParams; +// use crate::pagination::GetPagination; +// use crate::pagination::GetSort; + +fn with_db( + db_pool: impl DBRepository + DBOrganization, +) -> impl Filter + Clone { + warp::any().map(move || db_pool.clone()) +} + +pub fn routes(db_access: impl DBRepository + DBOrganization) -> BoxedFilter<(impl Reply,)> { + let repository = warp::path!("repositories"); // TODO: move this to the "organization" endpoint as a subendpoint + let repository_id = warp::path!("repositories" / i32); + // let repository_name = warp::path!("repositories" / "name" / String); + + let get_repositories = repository + .and(warp::get()) + .and(with_db(db_access.clone())) + .and(warp::query::()) + .and_then(handlers::get_repositories_handler); + + // let get_repository_by_name = repository_name + // .and(warp::get()) + // .and(with_db(db_access.clone())) + // .and(warp::query::()) + // .and_then(handlers::get_repository_handler_name); + + let get_repository = repository_id + .and(warp::get()) + .and(with_db(db_access.clone())) + .and_then(handlers::get_repository_handler); + + // let create_repository = repository + // .and(with_auth()) + // .and(warp::post()) + // .and(warp::body::json()) + // .and(with_db(db_access.clone())) + // .and_then(handlers::create_repository_handler); + + // let delete_repository = repository_id + // .and(with_auth()) + // .and(warp::delete()) + // .and(with_db(db_access.clone())) + // .and_then(handlers::delete_repository_handler); + + // let route = get_repositories + // .or(get_repository) + // .or(create_repository) + // .or(delete_repository) + // .or(get_repository_by_name); + + // route.boxed() + + get_repository.or(get_repositories).boxed() +} diff --git a/src/user/db.rs b/src/api/users/db.rs similarity index 100% rename from src/user/db.rs rename to src/api/users/db.rs diff --git a/src/user/errors.rs b/src/api/users/errors.rs similarity index 100% rename from src/user/errors.rs rename to src/api/users/errors.rs diff --git a/src/user/handlers.rs b/src/api/users/handlers.rs similarity index 100% rename from src/user/handlers.rs rename to src/api/users/handlers.rs diff --git a/src/user/mod.rs b/src/api/users/mod.rs similarity index 100% rename from src/user/mod.rs rename to src/api/users/mod.rs diff --git a/src/user/models.rs b/src/api/users/models.rs similarity index 100% rename from src/user/models.rs rename to src/api/users/models.rs diff --git a/src/user/routes.rs b/src/api/users/routes.rs similarity index 100% rename from src/user/routes.rs rename to src/api/users/routes.rs diff --git a/src/auth_error.rs b/src/auth/errors.rs similarity index 96% rename from src/auth_error.rs rename to src/auth/errors.rs index 01f0a8e..1a82ac8 100644 --- a/src/auth_error.rs +++ b/src/auth/errors.rs @@ -3,7 +3,7 @@ use std::fmt; use thiserror::Error; use warp::{http::StatusCode, reject::Reject, reply::Response, Reply}; -use crate::error_handler::ErrorResponse; +use crate::errors::ErrorResponse; #[derive(Clone, Error, Debug, Deserialize, PartialEq)] pub enum AuthenticationError { diff --git a/src/auth.rs b/src/auth/mod.rs similarity index 91% rename from src/auth.rs rename to src/auth/mod.rs index ac2bf4a..ee3cab2 100644 --- a/src/auth.rs +++ b/src/auth/mod.rs @@ -1,10 +1,16 @@ -use crate::auth_error::AuthenticationError; +use base64::Engine; use std::env; use warp::{ http::header::{HeaderMap, HeaderValue, AUTHORIZATION}, reject, Filter, Rejection, }; + +use self::errors::AuthenticationError; + +pub mod errors; + const BASIC: &str = "Basic "; + pub fn with_auth() -> impl Filter + Clone { warp::filters::header::headers_cloned() .and_then(authorize) @@ -13,7 +19,8 @@ pub fn with_auth() -> impl Filter + Clone { async fn authorize(headers: HeaderMap) -> Result<(), Rejection> { match token_from_header(&headers) { Ok(token) => { - let credentials = base64::decode(token) + let credentials = base64::prelude::BASE64_STANDARD + .decode(token) .map_err(|_| reject::custom(AuthenticationError::WrongCredentials))?; let credentials_str = String::from_utf8(credentials) .map_err(|_| reject::custom(AuthenticationError::WrongCredentials))?; diff --git a/src/db/errors.rs b/src/db/errors.rs index 1160fdb..43633b3 100644 --- a/src/db/errors.rs +++ b/src/db/errors.rs @@ -9,8 +9,6 @@ pub enum DBError { DBPoolConnection(PoolError), #[error("error executing DB query: {0}")] DBQuery(#[from] DieselError), - #[error("error initializing the database: {0}")] - DBInit(DieselError), #[error("error reading file: {0}")] ReadFile(#[from] std::io::Error), #[error("database operation timed out: {0}")] diff --git a/src/db/mod.rs b/src/db/mod.rs index f98c1d7..53c4b60 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,4 +1,3 @@ pub mod errors; pub mod pool; pub mod types; -pub mod utils; diff --git a/src/db/utils.rs b/src/db/utils.rs deleted file mode 100644 index a959a27..0000000 --- a/src/db/utils.rs +++ /dev/null @@ -1,24 +0,0 @@ -use super::pool::DBAccessor; -use regex::Regex; -use tokio::time::Duration; - -pub const DB_QUERY_TIMEOUT: Duration = Duration::from_secs(5); -pub const ASC: &str = "ASC"; -pub const DESC: &str = "DESC"; - -pub fn detect_sql_injection(input: &str) -> bool { - let sql_injection_pattern = Regex::new(r"(?i)(\b(?:select|insert|update|delete|drop|alter|create)\b.*\b(?:from|into|table|where)\b|\bunion\b.*\b(?:select|values)\b|\b(?:exec|execute)\b\s*\()").unwrap(); - sql_injection_pattern.is_match(input) -} - -pub fn sort_direction(descending: bool) -> String { - if descending { - DESC.to_string() - } else { - ASC.to_string() - } -} - -pub fn default_sort_direction() -> String { - ASC.to_string() -} diff --git a/src/error_handler.rs b/src/error_handler.rs deleted file mode 100644 index f6d243f..0000000 --- a/src/error_handler.rs +++ /dev/null @@ -1,91 +0,0 @@ -use serde::Serialize; -use serde_derive::Deserialize; -use std::convert::Infallible; -use warp::{hyper::StatusCode, Rejection, Reply}; - -use crate::{ - auth_error::AuthenticationError, - db::errors::DBError, - // organization::errors::OrganizationError, - // pagination::{PaginationError, SortError}, - // repository::{errors::RepositoryError, models::RepositorySortError}, - // user::{errors::UserError, models::UserSortError}, -}; - -#[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct ErrorResponse { - pub message: String, -} - -pub async fn error_handler(err: Rejection) -> std::result::Result { - // TODO: improve this - // if let Some(e) = err.find::() { - // Ok(e.clone().into_response()) - // } else if let Some(e) = err.find::() { - // Ok(e.clone().into_response()) - // } else if let Some(e) = err.find::() { - // Ok(e.clone().into_response()) - // } else if let Some(e) = err.find::() { - // Ok(e.clone().into_response()) - // } else if let Some(e) = err.find::() { - // Ok(e.clone().into_response()) - // } else if let Some(e) = err.find::() { - // Ok(e.clone().into_response()) - // } else if let Some(e) = err.find::() { - // Ok(e.clone().into_response()) - // } - if let Some(e) = err.find::() { - Ok(e.clone().into_response()) - } else if let Some(e) = err.find::() { - let (code, message) = match e { - DBError::DBPoolConnection(_) => ( - StatusCode::INTERNAL_SERVER_ERROR, - "Database connection error", - ), - DBError::DBQuery(_) => (StatusCode::BAD_REQUEST, "Database query failed"), - DBError::DBInit(_) => ( - StatusCode::INTERNAL_SERVER_ERROR, - "Error initializing database", - ), - DBError::ReadFile(_) => (StatusCode::INTERNAL_SERVER_ERROR, "File read error"), - DBError::DBTimeout(_) => (StatusCode::REQUEST_TIMEOUT, "Database operation timed out"), - }; - - let json = warp::reply::json(&ErrorResponse { - message: message.to_string(), - }); - - Ok(warp::reply::with_status(json, code).into_response()) - } else { - let code; - let message; - - if err.is_not_found() { - // Handle not found errors - code = StatusCode::NOT_FOUND; - message = "Not Found"; - } else if err - .find::() - .is_some() - { - // Handle invalid body errors - code = StatusCode::BAD_REQUEST; - message = "Invalid Body"; - } else if err.find::().is_some() { - // Handle method not allowed errors - code = StatusCode::METHOD_NOT_ALLOWED; - message = "Method Not Allowed"; - } else { - // Handle all other errors - eprintln!("Unhandled error: {:?}", err); - code = StatusCode::INTERNAL_SERVER_ERROR; - message = "Internal Server Error"; - } - - let json = warp::reply::json(&ErrorResponse { - message: message.into(), - }); - - Ok(warp::reply::with_status(json, code).into_response()) - } -} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..c46db16 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,69 @@ +use serde::Serialize; +use serde_derive::Deserialize; +use std::convert::Infallible; +use warp::{hyper::StatusCode, Rejection, Reply}; + +use crate::{ + auth::errors::AuthenticationError, + db::errors::DBError, + // pagination::{PaginationError, SortError}, + // repository::{errors::RepositoryError, models::RepositorySortError}, + // user::{errors::UserError, models::UserSortError}, +}; + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct ErrorResponse { + pub message: String, +} + +pub async fn error_handler(err: Rejection) -> std::result::Result { + let (status, message) = if err.is_not_found() { + (StatusCode::NOT_FOUND, "Resource not found".to_string()) + } else if err.find::().is_some() { + ( + StatusCode::METHOD_NOT_ALLOWED, + "Method not allowed".to_string(), + ) + } else if let Some(e) = err.find::() { + eprintln!("BodyDeserializeError error: {:?}", e); + (StatusCode::BAD_REQUEST, "Invalid request body".to_string()) + } else if let Some(e) = err.find::() { + eprintln!("InvalidQuery error: {:?}", e); + ( + StatusCode::BAD_REQUEST, + "Invalid query parameters".to_string(), + ) + } else if let Some(e) = err.find::() { + eprintln!("AuthenticationError: {}", e.to_string()); + ( + StatusCode::UNAUTHORIZED, + format!("AuthenticationError - {}", e.to_string()), + ) + } else if let Some(db_error) = err.find::() { + match db_error { + DBError::DBPoolConnection(_) => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Database connection error".to_string(), + ), + DBError::DBQuery(_) => (StatusCode::BAD_REQUEST, "Database query failed".to_string()), + DBError::ReadFile(_) => ( + StatusCode::INTERNAL_SERVER_ERROR, + "File read error".to_string(), + ), + DBError::DBTimeout(_) => ( + StatusCode::REQUEST_TIMEOUT, + "Database operation timed out".to_string(), + ), + } + } else { + eprintln!("Unhandled error: {:?}", err); // Ensure all unexpected errors are logged. + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Internal server error".to_string(), + ) + }; + + let json = warp::reply::json(&ErrorResponse { message }); + + Ok(warp::reply::with_status(json, status)) +} diff --git a/src/main.rs b/src/main.rs index eddff8f..b89e3b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,15 +2,12 @@ mod types; use crate::types::ApiConfig; +mod api; mod auth; -mod auth_error; mod db; -mod error_handler; -mod health; +mod errors; // mod languages; // mod issue; -// mod organization; -// mod pagination; // mod repository; // mod user; pub mod schema; diff --git a/src/organization/db.rs b/src/organization/db.rs deleted file mode 100644 index 481b56c..0000000 --- a/src/organization/db.rs +++ /dev/null @@ -1,80 +0,0 @@ -use mobc::async_trait; -use mobc_postgres::tokio_postgres::Row; -use warp::reject; - -use crate::db::{ - pool::DBAccess, - utils::{ - execute_query_with_timeout, query_one_timeout, query_opt_timeout, query_with_timeout, - DB_QUERY_TIMEOUT, - }, -}; - -use super::models::{Organization, OrganizationRequest}; - -const TABLE: &str = "organizations"; - -#[async_trait] -pub trait DBOrganization: Send + Sync + Clone + 'static { - async fn get_organization(&self, id: i32) -> Result, reject::Rejection>; - async fn get_organization_by_name( - &self, - name: &str, - ) -> Result, reject::Rejection>; - async fn get_organizations(&self) -> Result, reject::Rejection>; - async fn create_organization( - &self, - organization: OrganizationRequest, - ) -> Result; - async fn delete_organization(&self, id: i32) -> Result<(), reject::Rejection>; -} - -#[async_trait] -impl DBOrganization for DBAccess { - async fn get_organization(&self, id: i32) -> Result, reject::Rejection> { - let query = format!("SELECT * FROM {} WHERE id = $1", TABLE); - match query_opt_timeout(self, query.as_str(), &[&id], DB_QUERY_TIMEOUT).await? { - Some(organization) => Ok(Some(row_to_organization(&organization))), - None => Ok(None), - } - } - async fn get_organization_by_name( - &self, - name: &str, - ) -> Result, reject::Rejection> { - let query = format!("SELECT * FROM {} WHERE name = $1", TABLE); - match query_opt_timeout(self, query.as_str(), &[&name], DB_QUERY_TIMEOUT).await? { - Some(organization) => Ok(Some(row_to_organization(&organization))), - None => Ok(None), - } - } - - async fn get_organizations(&self) -> Result, reject::Rejection> { - let query = format!("SELECT * FROM {} ORDER BY created_at DESC", TABLE); - let rows = query_with_timeout(self, query.as_str(), &[], DB_QUERY_TIMEOUT).await?; - Ok(rows.iter().map(row_to_organization).collect()) - } - - async fn create_organization( - &self, - organization: OrganizationRequest, - ) -> Result { - let query = format!("INSERT INTO {} (name) VALUES ($1) RETURNING *", TABLE); - let row = query_one_timeout(self, &query, &[&organization.name], DB_QUERY_TIMEOUT).await?; - Ok(row_to_organization(&row)) - } - - async fn delete_organization(&self, id: i32) -> Result<(), reject::Rejection> { - let query = format!("DELETE FROM {} WHERE id = $1", TABLE); - execute_query_with_timeout(self, &query, &[&id], DB_QUERY_TIMEOUT).await - } -} - -fn row_to_organization(row: &Row) -> Organization { - let id: i32 = row.get(0); - let name: &str = row.get(1); - Organization { - id, - name: name.to_string(), - } -} diff --git a/src/organization/errors.rs b/src/organization/errors.rs deleted file mode 100644 index 5c3f04b..0000000 --- a/src/organization/errors.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::fmt; - -use serde_derive::Deserialize; -use thiserror::Error; -use warp::{ - http::StatusCode, - reject::Reject, - reply::{Reply, Response}, -}; - -use crate::error_handler::ErrorResponse; - -#[derive(Clone, Error, Debug, Deserialize, PartialEq)] -pub enum OrganizationError { - OrganizationExists(i32), - OrganizationNotFound(i32), -} - -impl fmt::Display for OrganizationError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - OrganizationError::OrganizationExists(id) => { - write!(f, "Organization #{} already exists", id) - } - OrganizationError::OrganizationNotFound(id) => { - write!(f, "Organization #{} not found", id) - } - } - } -} - -impl Reject for OrganizationError {} - -impl Reply for OrganizationError { - fn into_response(self) -> Response { - let code = match self { - OrganizationError::OrganizationExists(_) => StatusCode::BAD_REQUEST, - OrganizationError::OrganizationNotFound(_) => StatusCode::NOT_FOUND, - }; - let message = self.to_string(); - - let json = warp::reply::json(&ErrorResponse { message }); - - warp::reply::with_status(json, code).into_response() - } -} diff --git a/src/organization/handlers.rs b/src/organization/handlers.rs deleted file mode 100644 index 8524594..0000000 --- a/src/organization/handlers.rs +++ /dev/null @@ -1,64 +0,0 @@ -use warp::{ - http::StatusCode, - reject::Rejection, - reply::{json, Reply}, -}; - -use super::{ - db::DBOrganization, - errors::OrganizationError, - models::{OrganizationRequest, OrganizationResponse}, -}; - -pub async fn create_organization_handler( - body: OrganizationRequest, - db_access: impl DBOrganization, -) -> Result { - match db_access.get_organization_by_name(&body.name).await? { - Some(u) => Err(warp::reject::custom(OrganizationError::OrganizationExists( - u.id, - )))?, - None => Ok(json(&OrganizationResponse::of( - db_access.create_organization(body).await?, - ))), - } -} - -pub async fn get_organization_handler( - id: i32, - db_access: impl DBOrganization, -) -> Result { - match db_access.get_organization(id).await? { - None => Err(warp::reject::custom( - OrganizationError::OrganizationNotFound(id), - ))?, - Some(organization) => Ok(json(&OrganizationResponse::of(organization))), - } -} - -pub async fn get_organizations_handler( - db_access: impl DBOrganization, -) -> Result { - let organizations = db_access.get_organizations().await?; - Ok(json::>( - &organizations - .into_iter() - .map(OrganizationResponse::of) - .collect(), - )) -} - -pub async fn delete_organization_handler( - id: i32, - db_access: impl DBOrganization, -) -> Result { - match db_access.get_organization(id).await? { - Some(_) => { - let _ = &db_access.delete_organization(id).await?; - Ok(StatusCode::OK) - } - None => Err(warp::reject::custom( - OrganizationError::OrganizationNotFound(id), - ))?, - } -} diff --git a/src/organization/models.rs b/src/organization/models.rs deleted file mode 100644 index 43cd4c7..0000000 --- a/src/organization/models.rs +++ /dev/null @@ -1,27 +0,0 @@ -use serde_derive::{Deserialize, Serialize}; - -#[derive(Deserialize)] -pub struct Organization { - pub id: i32, - pub name: String, -} - -#[derive(Serialize, Deserialize)] -pub struct OrganizationRequest { - pub name: String, -} - -#[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct OrganizationResponse { - pub id: i32, - pub name: String, -} - -impl OrganizationResponse { - pub fn of(organization: Organization) -> OrganizationResponse { - OrganizationResponse { - id: organization.id, - name: organization.name, - } - } -} diff --git a/src/organization/routes.rs b/src/organization/routes.rs deleted file mode 100644 index cf1883d..0000000 --- a/src/organization/routes.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::convert::Infallible; - -use warp::filters::BoxedFilter; -use warp::{Filter, Reply}; - -use crate::auth::with_auth; - -use super::db::DBOrganization; -use super::handlers; - -fn with_db( - db_pool: impl DBOrganization, -) -> impl Filter + Clone { - warp::any().map(move || db_pool.clone()) -} - -pub fn routes(db_access: impl DBOrganization) -> BoxedFilter<(impl Reply,)> { - let organization = warp::path!("organizations"); - let organization_id = warp::path!("organizations" / i32); - - let get_organizations = organization - .and(warp::get()) - .and(with_db(db_access.clone())) - .and_then(handlers::get_organizations_handler); - - let get_organization = organization_id - .and(warp::get()) - // .and(warp::path::param()) - .and(with_db(db_access.clone())) - .and_then(handlers::get_organization_handler); - - let create_organization = organization - .and(with_auth()) - .and(warp::post()) - .and(warp::body::json()) - .and(with_db(db_access.clone())) - .and_then(handlers::create_organization_handler); - - let delete_organization = organization_id - .and(with_auth()) - .and(warp::delete()) - .and(with_db(db_access.clone())) - .and_then(handlers::delete_organization_handler); - - let route = get_organizations - .or(get_organization) - .or(create_organization) - .or(delete_organization); - - route.boxed() -} diff --git a/src/pagination.rs b/src/pagination.rs deleted file mode 100644 index dcd6150..0000000 --- a/src/pagination.rs +++ /dev/null @@ -1,159 +0,0 @@ -use std::fmt; - -use super::db::utils::detect_sql_injection; -use crate::error_handler::ErrorResponse; -use serde::{Deserialize, Serialize}; -use thiserror::Error; -use warp::{ - http::StatusCode, - reject::Reject, - reply::{Reply, Response}, -}; - -#[derive(Serialize, Deserialize, Clone)] -pub struct GetPagination { - pub limit: Option, - pub offset: Option, - // pub sort: Option, - // pub ascending: Option, -} - -impl GetPagination { - // A method to set default values for individual fields if they are None - pub fn validate(&self) -> Result { - let mut filters = self.clone(); - let default = Self::default(); - - if self.limit.is_none() { - filters.limit = default.limit; - } else { - let limit = self.limit.unwrap(); - if limit <= 0 || limit >= 1000 { - return Err(PaginationError::InvalidLimit(limit)); - } - } - if self.offset.is_none() { - filters.offset = default.offset; - } else { - let offset = self.offset.unwrap(); - if offset <= 0 { - return Err(PaginationError::InvalidOffset(offset)); - } - } - // if self.sort.is_none() { - // filters.sort = default.sort; - // } - // if self.ascending.is_none() { - // filters.ascending = default.ascending; - // } - Ok(filters) - } -} - -impl Default for GetPagination { - fn default() -> Self { - GetPagination { - limit: Some(1000), - offset: Some(0), - // sort: Some("users.id".to_string()), - // ascending: Some(true), - } - } -} - -#[derive(Clone, Error, Debug, Deserialize, PartialEq)] -pub enum PaginationError { - InvalidOffset(i64), - InvalidLimit(i64), -} - -impl fmt::Display for PaginationError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - PaginationError::InvalidOffset(offset) => { - write!(f, "Offset {} is invalid", offset) - } - PaginationError::InvalidLimit(limit) => { - write!(f, "Limit #{} not found", limit) - } - } - } -} - -impl Reject for PaginationError {} - -impl Reply for PaginationError { - fn into_response(self) -> Response { - let code = match self { - PaginationError::InvalidOffset(_) => StatusCode::BAD_REQUEST, - PaginationError::InvalidLimit(_) => StatusCode::BAD_REQUEST, - }; - let message = self.to_string(); - - let json = warp::reply::json(&ErrorResponse { message }); - - warp::reply::with_status(json, code).into_response() - } -} - -#[derive(Serialize, Deserialize, Default, Clone)] -pub struct GetSort { - pub sort_by: Option, - pub descending: Option, -} - -#[derive(Clone, Error, Debug, Deserialize, PartialEq)] -pub enum SortError { - InvalidSortBy, -} - -impl GetSort { - pub fn validate(&self) -> Result { - match (self.sort_by.clone(), self.descending) { - (None, None) => Ok(self.clone()), - (None, Some(_)) => Ok(Self { - sort_by: None, - descending: None, - }), - (Some(sort_by), some_or_none) => { - if detect_sql_injection(&sort_by) { - Err(SortError::InvalidSortBy) - } else { - Ok(Self { - sort_by: Some(sort_by), - descending: if some_or_none.is_none() { - Some(false) - } else { - some_or_none - }, - }) - } - } - } - } -} - -impl fmt::Display for SortError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - SortError::InvalidSortBy => { - write!(f, "Sort by is invalid") - } - } - } -} - -impl Reject for SortError {} - -impl Reply for SortError { - fn into_response(self) -> Response { - let code = match self { - SortError::InvalidSortBy => StatusCode::BAD_REQUEST, - }; - let message = self.to_string(); - - let json = warp::reply::json(&ErrorResponse { message }); - - warp::reply::with_status(json, code).into_response() - } -} diff --git a/src/repository/db.rs b/src/repository/db.rs deleted file mode 100644 index 2040bac..0000000 --- a/src/repository/db.rs +++ /dev/null @@ -1,174 +0,0 @@ -use mobc::async_trait; -use mobc_postgres::tokio_postgres::Row; -use warp::reject; - -use super::models::{NewRepository, RepositoriesRelations, Repository, RepositorySort}; -use crate::db::{ - pool::DBAccess, - utils::{ - execute_query_with_timeout, query_one_timeout, query_opt_timeout, query_with_timeout, - DB_QUERY_TIMEOUT, - }, -}; -use crate::pagination::GetPagination; - -const TABLE: &str = "repositories"; - -#[async_trait] -pub trait DBRepository: Send + Sync + Clone + 'static { - async fn get_repository( - &self, - id: i32, - relations: RepositoriesRelations, - ) -> Result, reject::Rejection>; - async fn get_repository_by_name( - &self, - name: &str, - relations: RepositoriesRelations, - ) -> Result, reject::Rejection>; - async fn get_repositories( - &self, - relations: RepositoriesRelations, - pagination: GetPagination, - sort: RepositorySort, - ) -> Result, reject::Rejection>; - async fn create_repository( - &self, - repository: NewRepository, - ) -> Result; - async fn delete_repository(&self, id: i32) -> Result<(), reject::Rejection>; -} - -#[async_trait] -impl DBRepository for DBAccess { - async fn get_repository( - &self, - id: i32, - relations: RepositoriesRelations, - ) -> Result, reject::Rejection> { - let mut query = format!("SELECT * FROM {} ", TABLE); - if relations.issues { - query += "LEFT JOIN issues on issues.repository_id = repositories.id "; - if relations.tips { - query += "LEFT JOIN tips on tips.id = issues.id "; - } - } - if relations.maintainers { - query += "LEFT JOIN maintainers on maintainers.repository_id = repositories.id "; - query += "LEFT JOIN users on maintainers.user_id = users.id "; - } - if relations.languages { - query += "LEFT JOIN languages on repositories.languages_id = languages.id "; - } - query += "WHERE id = $1"; - - match query_opt_timeout(self, query.as_str(), &[&id], DB_QUERY_TIMEOUT).await? { - Some(repository) => Ok(Some(row_to_repository(&repository))), - None => Ok(None), - } - } - async fn get_repository_by_name( - &self, - name: &str, - relations: RepositoriesRelations, - ) -> Result, reject::Rejection> { - let mut query = format!("SELECT * FROM {} ", TABLE); - if relations.issues { - query += "LEFT JOIN issues on issues.repository_id = repositories.id "; - if relations.tips { - query += "LEFT JOIN tips on tips.id = issues.id "; - } - } - if relations.maintainers { - query += "LEFT JOIN maintainers on maintainers.repository_id = repositories.id "; - query += "LEFT JOIN users on maintainers.user_id = users.id "; - } - if relations.languages { - query += "LEFT JOIN languages on repositories.languages_id = languages.id "; - } - query += "WHERE name = $1"; - match query_opt_timeout(self, query.as_str(), &[&name], DB_QUERY_TIMEOUT).await? { - Some(repository) => Ok(Some(row_to_repository(&repository))), - None => Ok(None), - } - } - - async fn get_repositories( - &self, - relations: RepositoriesRelations, - pagination: GetPagination, - sort: RepositorySort, - ) -> Result, reject::Rejection> { - let mut query = format!("SELECT * FROM {} ", TABLE); - if relations.issues { - query += "LEFT JOIN issues on issues.repository_id = repositories.id "; - if relations.tips { - query += "LEFT JOIN tips on tips.id = issues.id "; - } - } - if relations.maintainers { - query += "LEFT JOIN maintainers on maintainers.repository_id = repositories.id "; - query += "LEFT JOIN users on maintainers.user_id = users.id "; - } - if relations.languages { - query += "LEFT JOIN languages on repositories.languages_id = languages.id "; - } - query += &format!("ORDER BY {} {}", sort.field, sort.order); // cannot use $1 or $2 - - query += "LIMIT $1 OFFSET $2"; - let rows = query_with_timeout( - self, - query.as_str(), - &[&pagination.limit, &pagination.offset], - DB_QUERY_TIMEOUT, - ) - .await?; - Ok(rows.iter().map(row_to_repository).collect()) - } - - async fn create_repository( - &self, - repository: NewRepository, - ) -> Result { - let query = format!( - "INSERT INTO {} (name, icon, organization_id, url, e_tag) VALUES ($1, $2, $3, $4, $5) RETURNING *", - TABLE - ); - let row = query_one_timeout( - self, - &query, - &[ - &repository.name, - &repository.icon, - &repository.organization_id, - &repository.url, - &repository.e_tag, - ], - DB_QUERY_TIMEOUT, - ) - .await?; - Ok(row_to_repository(&row)) - } - - async fn delete_repository(&self, id: i32) -> Result<(), reject::Rejection> { - let query = format!("DELETE FROM {} WHERE id = $1", TABLE); - execute_query_with_timeout(self, &query, &[&id], DB_QUERY_TIMEOUT).await - } -} - -fn row_to_repository(row: &Row) -> Repository { - let id: i32 = row.get(0); - let name: &str = row.get(1); - let organization_id: i32 = row.get(2); - let icon: &str = row.get(3); - let url: &str = row.get(4); - let e_tag: &str = row.get(5); - Repository { - id, - name: name.to_string(), - organization_id, - icon: icon.to_string(), - url: url.to_string(), - e_tag: e_tag.to_string(), - } -} diff --git a/src/repository/handlers.rs b/src/repository/handlers.rs deleted file mode 100644 index a9bb85b..0000000 --- a/src/repository/handlers.rs +++ /dev/null @@ -1,120 +0,0 @@ -use warp::{ - http::StatusCode, - reject::Rejection, - reply::{json, with_status, Reply}, -}; - -use crate::{ - organization::{db::DBOrganization, errors::OrganizationError}, - pagination::GetSort, -}; - -use super::{ - db::DBRepository, - errors::RepositoryError, - models::{GetRepositoryQuery, NewRepository, RepositoriesRelations, RepositoryResponse}, -}; -use crate::pagination::GetPagination; -use crate::repository::models::RepositorySort; - -pub async fn create_repository_handler( - body: NewRepository, - db_access: impl DBRepository + DBOrganization, -) -> Result { - match db_access.get_organization(body.organization_id).await? { - Some(_) => match db_access - .get_repository_by_name(&body.name, RepositoriesRelations::default()) - .await? - { - Some(u) => Err(warp::reject::custom(RepositoryError::AlreadyExists(u.id)))?, - None => Ok(with_status( - json(&RepositoryResponse::of( - db_access.create_repository(body).await?, - )), - StatusCode::CREATED, - )), - }, - None => Err(warp::reject::custom( - OrganizationError::OrganizationNotFound(body.organization_id), - ))?, - } -} - -pub async fn get_repository_handler( - id: i32, - db_access: impl DBRepository, - query: GetRepositoryQuery, -) -> Result { - let relations = RepositoriesRelations { - tips: query.tips.unwrap_or_default(), - maintainers: query.maintainers.unwrap_or_default(), - issues: query.issues.unwrap_or_default(), - languages: query.languages.unwrap_or_default(), - }; - match db_access.get_repository(id, relations).await? { - None => Err(warp::reject::custom(RepositoryError::NotFound(id)))?, - Some(repository) => Ok(json(&RepositoryResponse::of(repository))), - } -} -pub async fn get_repository_handler_name( - name: String, - db_access: impl DBRepository, - query: GetRepositoryQuery, -) -> Result { - let relations = RepositoriesRelations { - tips: query.tips.unwrap_or_default(), - maintainers: query.maintainers.unwrap_or_default(), - issues: query.issues.unwrap_or_default(), - languages: query.languages.unwrap_or_default(), - }; - match db_access.get_repository_by_name(&name, relations).await? { - None => Err(warp::reject::custom(RepositoryError::NotFoundByName(name)))?, - Some(repository) => Ok(json(&RepositoryResponse::of(repository))), - } -} - -pub async fn get_repositories_handler( - db_access: impl DBRepository, - query: GetRepositoryQuery, - filters: GetPagination, - sort: GetSort, -) -> Result { - let relations = RepositoriesRelations { - tips: query.tips.unwrap_or_default(), - maintainers: query.maintainers.unwrap_or_default(), - issues: query.issues.unwrap_or_default(), - languages: query.languages.unwrap_or_default(), - }; - let pagination = filters.validate()?; - let sort = sort.validate()?; - let repository_sort = match (sort.sort_by, sort.descending) { - (Some(sort_by), Some(descending)) => RepositorySort::new(&sort_by, descending)?, - _ => RepositorySort::default(), - }; - - let repositories = db_access - .get_repositories(relations, pagination, repository_sort) - .await?; - Ok(json::>( - &repositories - .into_iter() - .map(RepositoryResponse::of) - .collect(), - )) -} - -pub async fn delete_repository_handler( - id: i32, - db_access: impl DBRepository, -) -> Result { - match db_access - .get_repository(id, RepositoriesRelations::default()) - .await? - { - Some(_) => { - let _ = &db_access.delete_repository(id).await?; - Ok(StatusCode::NO_CONTENT) - } - None => Err(warp::reject::custom(RepositoryError::NotFound(id)))?, - } -} diff --git a/src/repository/routes.rs b/src/repository/routes.rs deleted file mode 100644 index 30183bc..0000000 --- a/src/repository/routes.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::convert::Infallible; - -use warp::filters::BoxedFilter; -use warp::{Filter, Reply}; - -use crate::auth::with_auth; -use crate::organization::db::DBOrganization; - -use super::db::DBRepository; -use super::handlers; -use super::models::GetRepositoryQuery; -use crate::pagination::GetPagination; -use crate::pagination::GetSort; - -fn with_db( - db_pool: impl DBRepository + DBOrganization, -) -> impl Filter + Clone { - warp::any().map(move || db_pool.clone()) -} - -pub fn routes(db_access: impl DBRepository + DBOrganization) -> BoxedFilter<(impl Reply,)> { - let repository = warp::path!("repositories"); // TODO: move this to the "organization" endpoint as a subendpoint - let repository_id = warp::path!("repositories" / i32); - let repository_name = warp::path!("repositories" / "name" / String); - - let get_repositories = repository - .and(warp::get()) - .and(with_db(db_access.clone())) - .and(warp::query::()) - .and(warp::query::()) - .and(warp::query::()) - .and_then(handlers::get_repositories_handler); - - let get_repository_by_name = repository_name - .and(warp::get()) - .and(with_db(db_access.clone())) - .and(warp::query::()) - .and_then(handlers::get_repository_handler_name); - - let get_repository = repository_id - .and(warp::get()) - .and(with_db(db_access.clone())) - .and(warp::query::()) - .and_then(handlers::get_repository_handler); - - let create_repository = repository - .and(with_auth()) - .and(warp::post()) - .and(warp::body::json()) - .and(with_db(db_access.clone())) - .and_then(handlers::create_repository_handler); - - let delete_repository = repository_id - .and(with_auth()) - .and(warp::delete()) - .and(with_db(db_access.clone())) - .and_then(handlers::delete_repository_handler); - - let route = get_repositories - .or(get_repository) - .or(create_repository) - .or(delete_repository) - .or(get_repository_by_name); - - route.boxed() -} diff --git a/src/schema.rs b/src/schema.rs index f8576f1..f6d7675 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,243 +1,17 @@ // @generated automatically by Diesel CLI. -pub mod sql_types { - #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "tip_status"))] - pub struct TipStatus; - - #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "tip_type"))] - pub struct TipType; -} - -diesel::table! { - blockchains (id) { - id -> Int4, - #[max_length = 255] - name -> Nullable, - created_at -> Timestamptz, - updated_at -> Nullable, - } -} - -diesel::table! { - comments (id) { - id -> Int4, - wish_id -> Int4, - user_id -> Int4, - #[max_length = 255] - comment -> Nullable, - positive_votes -> Nullable, - negative_votes -> Nullable, - created_at -> Timestamptz, - updated_at -> Nullable, - } -} - -diesel::table! { - filter_values (id) { - id -> Int4, - filters_id -> Int4, - emoji -> Text, - #[max_length = 255] - name -> Nullable, - created_at -> Timestamptz, - updated_at -> Nullable, - } -} - -diesel::table! { - filters (id) { - id -> Int4, - #[max_length = 255] - name -> Nullable, - emoji -> Text, - created_at -> Timestamptz, - updated_at -> Nullable, - } -} - -diesel::table! { - issues (id) { - id -> Int4, - issue_number -> Nullable, - created_at -> Nullable, - updated_at -> Nullable, - repository_id -> Nullable, - } -} - -diesel::table! { - issues_labels (id) { - id -> Int4, - issue_id -> Nullable, - labels_id -> Nullable, - } -} - -diesel::table! { - labels (id) { - id -> Int4, - #[max_length = 255] - name -> Nullable, - created_at -> Timestamptz, - updated_at -> Nullable, - } -} - -diesel::table! { - languages (id) { - id -> Int4, - #[max_length = 255] - name -> Nullable, - created_at -> Timestamptz, - updated_at -> Nullable, - } -} - -diesel::table! { - maintainers (id) { - id -> Int4, - repository_id -> Nullable, - user_id -> Nullable, - } -} - -diesel::table! { - organizations (id) { - id -> Int4, - #[max_length = 255] - name -> Nullable, - created_at -> Nullable, - } -} - -diesel::table! { - repositories (id) { - id -> Int4, - #[max_length = 255] - name -> Nullable, - organization_id -> Nullable, - created_at -> Nullable, - } -} - -diesel::table! { - repositories_filters (id) { - id -> Int4, - repositories_id -> Nullable, - filters_id -> Nullable, - filter_values_id -> Nullable, - } -} - -diesel::table! { - repositories_languages (id) { - id -> Int4, - repositories_id -> Nullable, - languages_id -> Nullable, - } -} - diesel::table! { - repositories_topics (id) { - id -> Int4, - repositories_id -> Nullable, - topics_id -> Nullable, - } -} - -diesel::table! { - use diesel::sql_types::*; - use super::sql_types::TipStatus; - use super::sql_types::TipType; - - tips (id) { + projects (id) { id -> Int4, - status -> TipStatus, - #[sql_name = "type"] - type_ -> TipType, - amount -> Int8, - #[max_length = 48] - to -> Varchar, - #[max_length = 48] - from -> Varchar, #[max_length = 255] - transaction -> Nullable, - blockchain_id -> Nullable, - #[max_length = 255] - url -> Nullable, - contributor_id -> Int4, - curator_id -> Nullable, - created_at -> Timestamptz, - updated_at -> Nullable, - } -} - -diesel::table! { - topics (id) { - id -> Int4, + name -> Varchar, #[max_length = 255] - name -> Nullable, + slug -> Varchar, + categories -> Nullable>>, + purposes -> Nullable>>, + stack_levels -> Nullable>>, + technologies -> Nullable>>, created_at -> Timestamptz, - updated_at -> Nullable, + updated_at -> Nullable, } } - -diesel::table! { - users (id) { - id -> Int4, - #[max_length = 100] - username -> Nullable, - created_at -> Nullable, - } -} - -diesel::table! { - wishes (id) { - id -> Int4, - issues_id -> Int4, - created_at -> Timestamptz, - updated_at -> Nullable, - } -} - -diesel::joinable!(comments -> users (user_id)); -diesel::joinable!(comments -> wishes (wish_id)); -diesel::joinable!(filter_values -> filters (filters_id)); -diesel::joinable!(issues -> repositories (repository_id)); -diesel::joinable!(issues_labels -> issues (issue_id)); -diesel::joinable!(issues_labels -> labels (labels_id)); -diesel::joinable!(maintainers -> repositories (repository_id)); -diesel::joinable!(maintainers -> users (user_id)); -diesel::joinable!(repositories -> organizations (organization_id)); -diesel::joinable!(repositories_filters -> filter_values (filter_values_id)); -diesel::joinable!(repositories_filters -> filters (filters_id)); -diesel::joinable!(repositories_filters -> repositories (repositories_id)); -diesel::joinable!(repositories_languages -> languages (languages_id)); -diesel::joinable!(repositories_languages -> repositories (repositories_id)); -diesel::joinable!(repositories_topics -> repositories (repositories_id)); -diesel::joinable!(repositories_topics -> topics (topics_id)); -diesel::joinable!(tips -> blockchains (blockchain_id)); -diesel::joinable!(wishes -> issues (issues_id)); - -diesel::allow_tables_to_appear_in_same_query!( - blockchains, - comments, - filter_values, - filters, - issues, - issues_labels, - labels, - languages, - maintainers, - organizations, - repositories, - repositories_filters, - repositories_languages, - repositories_topics, - tips, - topics, - users, - wishes, -); diff --git a/src/tests/contribution.rs b/src/tests/contribution.rs deleted file mode 100644 index 5ff5c10..0000000 --- a/src/tests/contribution.rs +++ /dev/null @@ -1,493 +0,0 @@ -// #[cfg(test)] -// mod tests { -// use crate::{ -// handlers::{error_handler, ErrorResponse}, -// init_db, -// user::routes::routes, -// user::{ -// db::DBUser, -// models::{NewUser, User, UserResponse}, -// }, -// }; -// use mobc::async_trait; -// use warp::{reject, test::request, Filter}; - -// #[derive(Clone)] -// pub struct DBMockValues {} -// #[derive(Clone)] -// pub struct DBMockEmpty {} - -// #[async_trait] -// impl DBUser for DBMockValues { -// async fn get_user(&self, id: i32) -> Result, reject::Rejection> { -// Ok(Some(User { -// id, -// username: "username".to_string(), -// })) -// } -// async fn get_user_by_username( -// &self, -// username: &str, -// ) -> Result, reject::Rejection> { -// Ok(Some(User { -// id: 1, -// username: username.to_string(), -// })) -// } -// async fn get_users(&self) -> Result, reject::Rejection> { -// Ok(vec![User { -// id: 1, -// username: "username".to_string(), -// }]) -// } -// async fn create_user(&self, user: NewUser) -> Result { -// Ok(User { -// id: 1, -// username: user.username, -// }) -// } -// async fn delete_user(&self, _: i32) -> Result<(), reject::Rejection> { -// Ok(()) -// } -// } -// #[async_trait] -// impl DBUser for DBMockEmpty { -// async fn get_user(&self, _: i32) -> Result, reject::Rejection> { -// Ok(None) -// } -// async fn get_users(&self) -> Result, reject::Rejection> { -// Ok(vec![]) -// } -// async fn get_user_by_username( -// &self, -// _username: &str, -// ) -> Result, reject::Rejection> { -// Ok(None) -// } -// async fn create_user(&self, user: NewUser) -> Result { -// Ok(User { -// id: 1, -// username: user.username, -// }) -// } -// async fn delete_user(&self, _: i32) -> Result<(), reject::Rejection> { -// Ok(()) -// } -// } - -// #[tokio::test] -// async fn test_get_user_mock_db() { -// let id = 1; -// let r = routes(DBMockValues {}); -// let resp = request().path(&format!("/users/{id}")).reply(&r).await; -// assert_eq!(resp.status(), 200); -// let body = resp.into_body(); -// assert!(!body.is_empty()); -// let expected_response = UserResponse { -// id, -// username: "username".to_string(), -// }; -// let response: UserResponse = serde_json::from_slice(&body).unwrap(); -// assert_eq!(response, expected_response) -// } -// #[tokio::test] -// async fn test_get_user_not_found_mock_db() { -// let id = 1; -// let r = routes(DBMockEmpty {}).recover(error_handler); -// let resp = request().path(&format!("/users/{id}")).reply(&r).await; - -// assert_eq!(resp.status(), 404); -// let body = resp.into_body(); -// assert!(!body.is_empty()); - -// let expected_response = ErrorResponse { -// message: format!("User #{} not found", id), -// }; -// let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); - -// assert_eq!(response, expected_response) -// } - -// #[tokio::test] -// async fn test_get_users_mock_db() { -// let r = routes(DBMockValues {}); -// let resp = request().path(&format!("/users")).reply(&r).await; - -// assert_eq!(resp.status(), 200); -// let body = resp.into_body(); -// assert!(!body.is_empty()); -// let expected_response = vec![UserResponse { -// id: 1, -// username: "username".to_string(), -// }]; -// let response: Vec = serde_json::from_slice(&body).unwrap(); -// assert_eq!(response, expected_response) -// } - -// #[tokio::test] -// async fn test_get_users_empty_mock_db() { -// let r = routes(DBMockEmpty {}); -// let resp = request().path(&format!("/users")).reply(&r).await; -// assert_eq!(resp.status(), 200); - -// let body = resp.into_body(); -// let response: Vec = serde_json::from_slice(&body).unwrap(); -// let expected_response: Vec = vec![]; -// assert_eq!(response, expected_response); -// } - -// #[tokio::test] -// async fn test_create_user_mock_db() { -// let id = 1; -// let new_user = serde_json::to_vec(&UserResponse { -// id: 1, -// username: "username".to_string(), -// }) -// .unwrap(); -// let r = routes(DBMockEmpty {}); -// let resp = request() -// .body(new_user) -// .path(&"/users") -// .method("POST") -// .reply(&r) -// .await; -// assert_eq!(resp.status(), 200); -// let body = resp.into_body(); -// assert!(!body.is_empty()); - -// let expected_response = UserResponse { -// id, -// username: "username".to_string(), -// }; -// let response: UserResponse = serde_json::from_slice(&body).unwrap(); -// assert_eq!(response, expected_response) -// } - -// #[tokio::test] -// async fn test_create_user_already_exists_mock_db() { -// let id = 1; -// let new_user = serde_json::to_vec(&UserResponse { -// id: 1, -// username: "username".to_string(), -// }) -// .unwrap(); -// let r = routes(DBMockValues {}).recover(error_handler); -// let resp = request() -// .body(new_user) -// .path(&"/users") -// .method("POST") -// .reply(&r) -// .await; -// assert_eq!(resp.status(), 400); -// let body = resp.into_body(); -// assert!(!body.is_empty()); - -// let expected_response = ErrorResponse { -// message: format!("User #{} already exists", id), -// }; - -// let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); -// assert_eq!(response, expected_response) -// } - -// #[tokio::test] -// async fn test_delete_user_mock_db() { -// let id = 1; -// let r = routes(DBMockValues {}); -// let resp = request() -// .path(&format!("/users/{id}")) -// .method("DELETE") -// .reply(&r) -// .await; -// assert_eq!(resp.status(), 200); -// let body = resp.into_body(); -// assert!(body.is_empty()); -// } - -// #[tokio::test] -// async fn test_delete_user_does_not_exist_mock_db() { -// let id = 1; -// let new_user = serde_json::to_vec(&UserResponse { -// id: 1, -// username: "username".to_string(), -// }) -// .unwrap(); -// let r = routes(DBMockEmpty {}).recover(error_handler); -// let resp = request() -// .body(new_user) -// .path(&format!("/users/{id}")) -// .method("DELETE") -// .reply(&r) -// .await; - -// assert_eq!(resp.status(), 404); -// let body = resp.into_body(); -// assert!(!body.is_empty()); - -// let expected_response = ErrorResponse { -// message: format!("User #{} not found", id), -// }; -// let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); -// assert_eq!(response, expected_response) -// } - -// #[tokio::test] -// #[ignore] -// async fn test_create_user_db() { -// let id = 1; -// let new_user = serde_json::to_vec(&UserResponse { -// id: 1, -// username: "username".to_string(), -// }) -// .unwrap(); -// let db = init_db( -// "postgres://postgres:password@localhost:5432/database".to_string(), -// "db_test.sql".to_string(), -// ) -// .await -// .unwrap(); -// let r = routes(db); -// let resp = request() -// .body(new_user) -// .path(&"/users") -// .method("POST") -// .reply(&r) -// .await; -// assert_eq!(resp.status(), 200); -// let body = resp.into_body(); -// assert!(!body.is_empty()); - -// let expected_response = UserResponse { -// id, -// username: "username".to_string(), -// }; -// let response: UserResponse = serde_json::from_slice(&body).unwrap(); -// assert_eq!(response, expected_response) -// } - -// #[tokio::test] -// #[ignore] -// async fn test_create_user_already_exists_db() { -// let id = 1; -// let new_user = serde_json::to_vec(&UserResponse { -// id: 1, -// username: "username".to_string(), -// }) -// .unwrap(); -// let db = init_db( -// "postgres://postgres:password@localhost:5432/database".to_string(), -// "db_test.sql".to_string(), -// ) -// .await -// .unwrap(); -// let r = routes(db).recover(error_handler); -// let _ = request() -// .body(new_user.clone()) -// .path(&"/users") -// .method("POST") -// .reply(&r) -// .await; -// let resp = request() -// .body(new_user) -// .path(&"/users") -// .method("POST") -// .reply(&r) -// .await; -// assert_eq!(resp.status(), 400); -// let body = resp.into_body(); -// assert!(!body.is_empty()); - -// let expected_response = ErrorResponse { -// message: format!("User #{} already exists", id), -// }; - -// let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); -// assert_eq!(response, expected_response) -// } - -// #[tokio::test] -// #[ignore] -// async fn test_get_user_not_found_db() { -// let id = 1; -// let db = init_db( -// "postgres://postgres:password@localhost:5432/database".to_string(), -// "db_test.sql".to_string(), -// ) -// .await -// .unwrap(); -// let r = routes(db).recover(error_handler); -// let resp = request().path(&format!("/users/{id}")).reply(&r).await; - -// assert_eq!(resp.status(), 404); -// let body = resp.into_body(); -// assert!(!body.is_empty()); - -// let expected_response = ErrorResponse { -// message: "User #1 not found".to_string(), -// }; -// let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); - -// assert_eq!(response, expected_response) -// } - -// #[tokio::test] -// #[ignore] -// async fn test_get_user_db() { -// let id = 1; -// let new_user = serde_json::to_vec(&UserResponse { -// id: 1, -// username: "username".to_string(), -// }) -// .unwrap(); -// let db = init_db( -// "postgres://postgres:password@localhost:5432/database".to_string(), -// "db_test.sql".to_string(), -// ) -// .await -// .unwrap(); -// let r = routes(db); -// let _ = request() -// .body(new_user) -// .path(&"/users") -// .method("POST") -// .reply(&r) -// .await; -// let resp = request().path(&format!("/users/{id}")).reply(&r).await; -// // assert_eq!(resp.status(), 200); -// let body = resp.into_body(); -// assert!(!body.is_empty()); -// let expected_response = UserResponse { -// id, -// username: "username".to_string(), -// }; -// let response: UserResponse = serde_json::from_slice(&body).unwrap(); -// assert_eq!(response, expected_response) -// } - -// #[tokio::test] -// #[ignore] -// async fn test_get_users_empty_db() { -// let db = init_db( -// "postgres://postgres:password@localhost:5432/database".to_string(), -// "db_test.sql".to_string(), -// ) -// .await -// .unwrap(); -// let r = routes(db).recover(error_handler); -// let resp = request().path(&format!("/users")).reply(&r).await; - -// assert_eq!(resp.status(), 200); -// let body = resp.into_body(); -// let response: Vec = serde_json::from_slice(&body).unwrap(); -// let expected_response: Vec = vec![]; -// assert_eq!(response, expected_response); -// } -// #[tokio::test] -// #[ignore] -// async fn test_get_users_db() { -// let id = 1; -// let new_user = serde_json::to_vec(&UserResponse { -// id: 1, -// username: "username".to_string(), -// }) -// .unwrap(); -// let db = init_db( -// "postgres://postgres:password@localhost:5432/database".to_string(), -// "db_test.sql".to_string(), -// ) -// .await -// .unwrap(); -// let r = routes(db); -// let _ = request() -// .body(new_user) -// .path(&"/users") -// .method("POST") -// .reply(&r) -// .await; -// let resp = request().path(&format!("/users")).reply(&r).await; -// assert_eq!(resp.status(), 200); - -// let body = resp.into_body(); -// assert!(!body.is_empty()); -// let expected_response = vec![UserResponse { -// id, -// username: "username".to_string(), -// }]; -// let response: Vec = serde_json::from_slice(&body).unwrap(); -// assert_eq!(response, expected_response) -// } - -// #[tokio::test] -// #[ignore] -// async fn test_delete_user_db() { -// let id = 1; -// let new_user = serde_json::to_vec(&UserResponse { -// id: 1, -// username: "username".to_string(), -// }) -// .unwrap(); -// let db = init_db( -// "postgres://postgres:password@localhost:5432/database".to_string(), -// "db_test.sql".to_string(), -// ) -// .await -// .unwrap(); -// let r = routes(db).recover(error_handler); -// let resp = request() -// .body(new_user) -// .path(&"/users") -// .method("POST") -// .reply(&r) -// .await; -// assert_eq!(resp.status(), 200); - -// let resp = request() -// .path(&format!("/users/{id}")) -// .method("DELETE") -// .reply(&r) -// .await; -// assert_eq!(resp.status(), 200); -// let body = resp.into_body(); -// assert!(body.is_empty()); - -// let resp = request().path(&format!("/users/{id}")).reply(&r).await; - -// assert_eq!(resp.status(), 404); -// let body = resp.into_body(); -// assert!(!body.is_empty()); -// } - -// #[tokio::test] -// #[ignore] -// async fn test_delete_user_does_not_exist_db() { -// let id = 1; -// let db = init_db( -// "postgres://postgres:password@localhost:5432/database".to_string(), -// "db_test.sql".to_string(), -// ) -// .await -// .unwrap(); -// let new_user = serde_json::to_vec(&UserResponse { -// id: 1, -// username: "username".to_string(), -// }) -// .unwrap(); -// let r = routes(db).recover(error_handler); -// let resp = request() -// .body(new_user) -// .path(&format!("/users/{id}")) -// .method("DELETE") -// .reply(&r) -// .await; - -// assert_eq!(resp.status(), 404); -// let body = resp.into_body(); -// assert!(!body.is_empty()); - -// let expected_response = ErrorResponse { -// message: "User #1 not found".to_string(), -// }; -// let response: ErrorResponse = serde_json::from_slice(&body).unwrap(); -// assert_eq!(response, expected_response) -// } -// } diff --git a/src/tests/error_handler.rs b/src/tests/error_handler.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/tests/health.rs b/src/tests/health.rs index 819c908..8302308 100644 --- a/src/tests/health.rs +++ b/src/tests/health.rs @@ -1,15 +1,18 @@ #[cfg(test)] mod tests { - use crate::health::{db::DBHealth, routes::routes}; - use mobc::async_trait; - use warp::{reject, test::request}; + use crate::{ + api::health::{db::DBHealth, routes::routes}, + db::errors::DBError, + types::ApiConfig, + utils::setup_db, + }; + use warp::test::request; #[derive(Clone)] pub struct DBMock {} - #[async_trait] impl DBHealth for DBMock { - async fn health(&self) -> Result<(), reject::Rejection> { + fn health(&self) -> Result<(), DBError> { Ok(()) } } @@ -21,23 +24,14 @@ mod tests { assert_eq!(resp.status(), 200); assert!(resp.body().is_empty()); } - // TODO: fix - // #[tokio::test] - // #[ignore] - // async fn test_health_db() { - // let db = init_db( - // "postgres://postgres:password@localhost:5432/database".to_string(), - // "db.sql".to_string(), - // ) - // .await - // .unwrap(); - // let r = routes(db); - // let resp = request().path("/health").reply(&r).await; - // assert_eq!(resp.status(), 200); - // assert!(resp.body().is_empty()); - // } + #[tokio::test] + async fn test_health_db() { + let ApiConfig { database_url, .. } = ApiConfig::new(); + let db = setup_db(&database_url).await; + let r = routes(db); + let resp = request().path("/health").reply(&r).await; + assert_eq!(resp.status(), 200); + assert!(resp.body().is_empty()); + } } - -// TODO: add e2e test using a real http server. -// hyper can be used for that diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 33e6c7d..43a7c76 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,4 +1 @@ -pub mod contribution; pub mod health; -pub mod repositories; -pub mod users; diff --git a/src/tests/users.rs b/src/tests/users.rs index f67025c..db1761c 100644 --- a/src/tests/users.rs +++ b/src/tests/users.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod tests { use crate::{ - error_handler::{self, ErrorResponse}, + errors::{self, ErrorResponse}, pagination::GetPagination, repository::{ db::DBRepository, @@ -122,7 +122,7 @@ mod tests { #[tokio::test] async fn test_get_user_by_id_not_found() { let id = 2; - let r = routes(UsersDBMock {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(errors::error_handler); let resp = request().path(&format!("/users/{id}")).reply(&r).await; assert_eq!(resp.status(), 404); let body = resp.into_body(); @@ -140,7 +140,7 @@ mod tests { #[tokio::test] async fn test_get_user_by_id_exists() { let id = 1; - let r = routes(UsersDBMock {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(errors::error_handler); let resp = request().path(&format!("/users/{id}")).reply(&r).await; assert_eq!(resp.status(), 200); let body = resp.into_body(); @@ -156,7 +156,7 @@ mod tests { #[tokio::test] async fn test_get_user_by_name_not_found() { let name = "not_found"; - let r = routes(UsersDBMock {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(errors::error_handler); let resp = request() .path(&format!("/users/username/{name}")) .reply(&r) @@ -176,7 +176,7 @@ mod tests { #[tokio::test] async fn test_get_user_by_name_exists() { let name = "username"; - let r = routes(UsersDBMock {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(errors::error_handler); let resp = request() .path(&format!("/users/username/{name}")) .reply(&r) @@ -192,7 +192,7 @@ mod tests { } #[tokio::test] async fn test_get_users() { - let r = routes(UsersDBMock {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(errors::error_handler); let resp = request().path(&format!("/users")).reply(&r).await; assert_eq!(resp.status(), 200); let body = resp.into_body(); @@ -201,7 +201,7 @@ mod tests { } #[tokio::test] async fn test_get_users_valid_query_params() { - let r = routes(UsersDBMock {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(errors::error_handler); let resp = request().path(&format!("/users?wishes=false&tips=false&maintainers=false&issues=false&limit=1&offset=10&sort_by=id&descending=false")).reply(&r).await; assert_eq!(resp.status(), 200); let body = resp.into_body(); @@ -210,7 +210,7 @@ mod tests { } #[tokio::test] async fn test_get_users_invalid_query_params() { - let r = routes(UsersDBMock {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(errors::error_handler); let resp = request() .path(&format!("/users?wishes=fal1se")) .reply(&r) @@ -256,7 +256,7 @@ mod tests { repositories: None, }) .unwrap(); - let r = routes(UsersDBMock {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(errors::error_handler); let resp = request() .body(new_user) .path(&"/users") @@ -279,7 +279,7 @@ mod tests { repositories: vec![1], }) .unwrap(); - let r = routes(UsersDBMock {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(errors::error_handler); let resp = request() .body(new_user) .path(&"/users/1/maintainers") @@ -307,7 +307,7 @@ mod tests { repositories: None, }) .unwrap(); - let r = routes(UsersDBMock {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(errors::error_handler); let resp = request() .body(new_user) .path(&"/users") @@ -331,7 +331,7 @@ mod tests { #[tokio::test] async fn test_delete_user() { let id = 1; - let r = routes(UsersDBMock {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(errors::error_handler); let resp = request() .path(&format!("/users/{id}")) .method("DELETE") @@ -347,7 +347,7 @@ mod tests { async fn test_delete_user_does_not_exist_mock_db() { let id = 4; - let r = routes(UsersDBMock {}).recover(error_handler::error_handler); + let r = routes(UsersDBMock {}).recover(errors::error_handler); let resp = request() .path(&format!("/users/4")) .method("DELETE") diff --git a/src/types.rs b/src/types.rs index b0477f9..9d50fe3 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,4 +1,5 @@ use dotenv::dotenv; +use serde::Deserialize; use std::env; /// Configuration used by this API. @@ -25,3 +26,19 @@ impl ApiConfig { } } } + +#[derive(Deserialize, Debug)] +pub struct PaginationParams { + #[serde(default = "default_limit")] + pub limit: i64, + #[serde(default = "default_offset")] + pub offset: i64, +} + +fn default_limit() -> i64 { + 100 // Default limit +} + +fn default_offset() -> i64 { + 0 // Default offset +} diff --git a/src/utils.rs b/src/utils.rs index 8b1b6f6..82b5a8e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,13 +2,13 @@ use ::warp::Reply; use warp::{filters::BoxedFilter, Filter}; use crate::{ + api::{health, projects}, db::{ self, errors::DBError, pool::{DBAccess, DBAccessor}, }, - error_handler::error_handler, - health, + errors::error_handler, }; pub async fn setup_db(url: &String) -> DBAccess { @@ -41,17 +41,19 @@ pub async fn setup_db(url: &String) -> DBAccess { pub fn setup_filters(db: DBAccess) -> BoxedFilter<(impl Reply,)> { let health_route = health::routes::routes(db.clone()); - // let repositories_route = repository::routes::routes(db.clone()); - - let error_handler = error_handler; + let projects_route = projects::routes::routes(db.clone()); health_route - // .or(repositories_route) + .or(projects_route) .with(warp::cors().allow_any_origin()) .recover(error_handler) .boxed() } -pub fn parse_ids(s: &str) -> Vec { - s.split(",").map(|id| id.parse::().unwrap()).collect() +// pub fn parse_ids(s: &str) -> Vec { +// s.split(",").map(|id| id.parse::().unwrap()).collect() // TODO: Handle errors, remove unwrap() +// } + +pub fn parse_comma_values(s: &str) -> Vec { + s.split(",").map(|el: &str| el.to_string()).collect() } From 66cac3ab950e0586d3cfaef908849e504cef29d7 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 5 May 2024 17:36:09 -0300 Subject: [PATCH 24/98] chore: add repository table --- migrations/2024-04-13-204151_init/down.sql | 3 ++- migrations/2024-04-13-204151_init/up.sql | 17 +++++++++++++---- src/schema.rs | 20 ++++++++++++++++++++ 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/migrations/2024-04-13-204151_init/down.sql b/migrations/2024-04-13-204151_init/down.sql index e9b1a24..ab870b3 100644 --- a/migrations/2024-04-13-204151_init/down.sql +++ b/migrations/2024-04-13-204151_init/down.sql @@ -1,2 +1,3 @@ -- Drop tables -DROP TABLE IF EXISTS projects; \ No newline at end of file +DROP TABLE IF EXISTS projects; +DROP TABLE IF EXISTS repositories; \ No newline at end of file diff --git a/migrations/2024-04-13-204151_init/up.sql b/migrations/2024-04-13-204151_init/up.sql index b9bd02f..ecc8de0 100644 --- a/migrations/2024-04-13-204151_init/up.sql +++ b/migrations/2024-04-13-204151_init/up.sql @@ -3,10 +3,19 @@ CREATE TABLE IF NOT EXISTS projects ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, slug VARCHAR(255) NOT NULL UNIQUE, - categories TEXT[], - purposes TEXT[], - stack_levels TEXT[], - technologies TEXT[], + categories TEXT [], + purposes TEXT [], + stack_levels TEXT [], + technologies TEXT [], created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NULL ); +-- basic github repository +CREATE TABLE IF NOT EXISTS repositories ( + id SERIAL PRIMARY KEY, + slug VARCHAR(255) NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL, + project_id INT REFERENCES projects(id), + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE NULL +); \ No newline at end of file diff --git a/src/schema.rs b/src/schema.rs index f6d7675..b41eb2a 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -15,3 +15,23 @@ diesel::table! { updated_at -> Nullable, } } + +diesel::table! { + repositories (id) { + id -> Int4, + #[max_length = 255] + slug -> Varchar, + #[max_length = 255] + name -> Varchar, + project_id -> Nullable, + created_at -> Timestamptz, + updated_at -> Nullable, + } +} + +diesel::joinable!(repositories -> projects (project_id)); + +diesel::allow_tables_to_appear_in_same_query!( + projects, + repositories, +); From def324ef27018c590e69a1395b480836e2f171f1 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 5 May 2024 20:05:52 -0300 Subject: [PATCH 25/98] refactor: repository --- migrations/2024-04-13-204151_init/up.sql | 9 +- src/api/mod.rs | 2 +- src/api/repositories/db.rs | 235 ++++++++--------------- src/api/repositories/handlers.rs | 122 +++++------- src/api/repositories/models.rs | 164 +++------------- src/api/repositories/routes.rs | 65 +++---- src/main.rs | 1 - src/schema.rs | 15 +- src/utils.rs | 16 +- 9 files changed, 216 insertions(+), 413 deletions(-) diff --git a/migrations/2024-04-13-204151_init/up.sql b/migrations/2024-04-13-204151_init/up.sql index ecc8de0..15faf35 100644 --- a/migrations/2024-04-13-204151_init/up.sql +++ b/migrations/2024-04-13-204151_init/up.sql @@ -10,12 +10,19 @@ CREATE TABLE IF NOT EXISTS projects ( created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NULL ); +CREATE TABLE IF NOT EXISTS languages ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) UNIQUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, + updated_at TIMESTAMP NULL +); -- basic github repository CREATE TABLE IF NOT EXISTS repositories ( id SERIAL PRIMARY KEY, slug VARCHAR(255) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, - project_id INT REFERENCES projects(id), + language_id INT REFERENCES languages(id) NOT NULL, + project_id INT REFERENCES projects(id) NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NULL ); \ No newline at end of file diff --git a/src/api/mod.rs b/src/api/mod.rs index 6d634d9..1af8945 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -2,5 +2,5 @@ pub mod health; // pub mod issue; // pub mod languages; pub mod projects; -// pub mod repositories; +pub mod repositories; // pub mod users; diff --git a/src/api/repositories/db.rs b/src/api/repositories/db.rs index 4d68f41..3913c0a 100644 --- a/src/api/repositories/db.rs +++ b/src/api/repositories/db.rs @@ -1,185 +1,108 @@ -use diesel::{associations::HasTable, prelude::*}; -use warp::reject; +use diesel::{dsl::now, prelude::*}; -use super::models::{NewRepository, RepositoriesRelations, Repository, RepositoryQueryParams}; -use crate::schema::languages::dsl::*; +use super::models::{NewRepository, QueryParams, Repository, UpdateRepository}; use crate::schema::repositories::dsl as repositories_dsl; -use crate::schema::repositories::dsl::*; use crate::utils; use crate::{ db::{ errors::DBError, pool::{DBAccess, DBAccessor}, }, - languages::models::Language, + types::PaginationParams, }; -// use crate::pagination::GetPagination; - -const TABLE: &str = "repositories"; pub trait DBRepository: Send + Sync + Clone + 'static { - fn get_repository(&self, id: i32) -> Result, reject::Rejection>; - fn get_repositories( + fn by_id(&self, id: i32) -> Result, DBError>; + fn all( &self, - params: RepositoryQueryParams, - ) -> Result, reject::Rejection>; - // async fn get_repository_by_name( - // &self, - // name: &str, - // relations: RepositoriesRelations, - // ) -> Result, reject::Rejection>; - // async fn get_repositories( - // &self, - // relations: RepositoriesRelations, - // pagination: GetPagination, - // sort: RepositorySort, - // ) -> Result, reject::Rejection>; - // async fn create_repository( - // &self, - // repository: NewRepository, - // ) -> Result; - // async fn delete_repository(&self, id: i32) -> Result<(), reject::Rejection>; + params: QueryParams, + pagination: PaginationParams, + ) -> Result, DBError>; + fn create(&self, repo: &NewRepository) -> Result; + fn update(&self, id: i32, repo: &UpdateRepository) -> Result; + fn delete(&self, id: i32) -> Result<(), DBError>; + fn by_slug(&self, slug: &str) -> Result, DBError>; } impl DBRepository for DBAccess { - // async fn get_repository( - // &self, - // id: i32, - // relations: RepositoriesRelations, - // ) -> Result, reject::Rejection> { - // let mut query = format!("SELECT * FROM {} ", TABLE); - // if relations.issues { - // query += "LEFT JOIN issues on issues.repository_id = repositories.id "; - // if relations.tips { - // query += "LEFT JOIN tips on tips.id = issues.id "; - // } - // } - // if relations.maintainers { - // query += "LEFT JOIN maintainers on maintainers.repository_id = repositories.id "; - // query += "LEFT JOIN users on maintainers.user_id = users.id "; - // } - // if relations.languages { - // query += "LEFT JOIN languages on repositories.languages_id = languages.id "; - // } - // query += "WHERE id = $1"; - - // match query_opt_timeout(self, query.as_str(), &[&id], DB_QUERY_TIMEOUT).await? { - // Some(repository) => Ok(Some(row_to_repository(&repository))), - // None => Ok(None), - // } - // } - - fn get_repositories( + fn all( &self, - params: RepositoryQueryParams, - ) -> Result, reject::Rejection> { + params: QueryParams, + pagination: PaginationParams, + ) -> Result, DBError> { let conn = &mut self.get_db_conn(); let mut query = repositories_dsl::repositories.into_boxed(); - if let Some(language_ids) = params.language { - let ids: Vec = utils::parse_ids(&language_ids); // TODO: handle parsing error - query = query.filter(repositories_dsl::language_id.eq_any(ids)); + if let Some(language_id) = params.language_ids { + let ids: Vec = utils::parse_ids(&language_id); + if ids.len() > 0 { + query = query.filter(repositories_dsl::language_id.eq_any(ids)); + } + } + if let Some(project_id) = params.project_ids { + let ids: Vec = utils::parse_ids(&project_id); + if ids.len() > 0 { + query = query.filter(repositories_dsl::language_id.eq_any(ids)); + } } - query - .load::(conn) - .map_err(|e| reject::custom(DBError::DBQuery(e))) + query = query.offset(pagination.offset).limit(pagination.limit); + + let result = query.load::(conn)?; + Ok(result) } - fn get_repository(&self, repo_id: i32) -> Result, reject::Rejection> { + fn by_id(&self, id: i32) -> Result, DBError> { let conn = &mut self.get_db_conn(); - repositories - .find(repo_id) + + let result = repositories_dsl::repositories + .find(id) .first::(conn) .optional() - .map_err(|e| reject::custom(DBError::DBQuery(e))) + .map_err(DBError::from)?; + + Ok(result) + } + + fn create(&self, repository: &NewRepository) -> Result { + let conn = &mut self.get_db_conn(); + + let repository = diesel::insert_into(repositories_dsl::repositories) + .values(repository) + .get_result(conn) + .map_err(DBError::from)?; + + Ok(repository) } - // async fn get_repository_by_name( - // &self, - // name: &str, - // relations: RepositoriesRelations, - // ) -> Result, reject::Rejection> { - // let mut query = format!("SELECT * FROM {} ", TABLE); - // if relations.issues { - // query += "LEFT JOIN issues on issues.repository_id = repositories.id "; - // if relations.tips { - // query += "LEFT JOIN tips on tips.id = issues.id "; - // } - // } - // if relations.maintainers { - // query += "LEFT JOIN maintainers on maintainers.repository_id = repositories.id "; - // query += "LEFT JOIN users on maintainers.user_id = users.id "; - // } - // if relations.languages { - // query += "LEFT JOIN languages on repositories.languages_id = languages.id "; - // } - // query += "WHERE name = $1"; - // match query_opt_timeout(self, query.as_str(), &[&name], DB_QUERY_TIMEOUT).await? { - // Some(repository) => Ok(Some(row_to_repository(&repository))), - // None => Ok(None), - // } - // } - - // async fn get_repositories( - // &self, - // relations: RepositoriesRelations, - // pagination: GetPagination, - // sort: RepositorySort, - // ) -> Result, reject::Rejection> { - // let mut query = format!("SELECT * FROM {} ", TABLE); - // if relations.issues { - // query += "LEFT JOIN issues on issues.repository_id = repositories.id "; - // if relations.tips { - // query += "LEFT JOIN tips on tips.id = issues.id "; - // } - // } - // if relations.maintainers { - // query += "LEFT JOIN maintainers on maintainers.repository_id = repositories.id "; - // query += "LEFT JOIN users on maintainers.user_id = users.id "; - // } - // if relations.languages { - // query += "LEFT JOIN languages on repositories.languages_id = languages.id "; - // } - // query += &format!("ORDER BY {} {}", sort.field, sort.order); // cannot use $1 or $2 - - // query += "LIMIT $1 OFFSET $2"; - // let rows = query_with_timeout( - // self, - // query.as_str(), - // &[&pagination.limit, &pagination.offset], - // DB_QUERY_TIMEOUT, - // ) - // .await?; - // Ok(rows.iter().map(row_to_repository).collect()) - // } - - // async fn create_repository( - // &self, - // repository: NewRepository, - // ) -> Result { - // let query = format!( - // "INSERT INTO {} (name, icon, organization_id, url, e_tag) VALUES ($1, $2, $3, $4, $5) RETURNING *", - // TABLE - // ); - // let row = query_one_timeout( - // self, - // &query, - // &[ - // &repository.name, - // &repository.icon, - // &repository.organization_id, - // &repository.url, - // &repository.e_tag, - // ], - // DB_QUERY_TIMEOUT, - // ) - // .await?; - // Ok(row_to_repository(&row)) - // } - - // async fn delete_repository(&self, id: i32) -> Result<(), reject::Rejection> { - // let query = format!("DELETE FROM {} WHERE id = $1", TABLE); - // execute_query_with_timeout(self, &query, &[&id], DB_QUERY_TIMEOUT).await - // } + fn update(&self, id: i32, repository: &UpdateRepository) -> Result { + let conn = &mut self.get_db_conn(); + + let project = + diesel::update(repositories_dsl::repositories.filter(repositories_dsl::id.eq(id))) + .set((repository, repositories_dsl::updated_at.eq(now))) + .get_result::(conn) + .map_err(DBError::from)?; + + Ok(project) + } + fn delete(&self, id: i32) -> Result<(), DBError> { + let conn = &mut self.get_db_conn(); + diesel::delete(repositories_dsl::repositories.filter(repositories_dsl::id.eq(id))) + .execute(conn) + .map_err(DBError::from)?; + + Ok(()) + } + fn by_slug(&self, slug: &str) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + + let result = repositories_dsl::repositories + .filter(repositories_dsl::slug.eq(slug)) + .first::(conn) + .optional() + .map_err(DBError::from)?; + + Ok(result) + } } diff --git a/src/api/repositories/handlers.rs b/src/api/repositories/handlers.rs index 8b863fc..ff96fbf 100644 --- a/src/api/repositories/handlers.rs +++ b/src/api/repositories/handlers.rs @@ -4,97 +4,61 @@ use warp::{ reply::{json, with_status, Reply}, }; -// use crate::{ -// organization::{db::DBOrganization, errors::OrganizationError}, -// pagination::GetSort, -// }; +use crate::types::PaginationParams; use super::{ db::DBRepository, errors::RepositoryError, - models::{ - GetRepositoryQuery, NewRepository, RepositoriesRelations, RepositoryQueryParams, - RepositoryResponse, - }, + models::{NewRepository, QueryParams, UpdateRepository}, }; -// use crate::pagination::GetPagination; -use crate::repository::models::RepositorySort; -// pub async fn create_repository_handler( -// body: NewRepository, -// db_access: impl DBRepository + DBOrganization, -// ) -> Result { -// match db_access.get_organization(body.organization_id).await? { -// Some(_) => match db_access -// .get_repository_by_name(&body.name, RepositoriesRelations::default()) -// .await? -// { -// Some(u) => Err(warp::reject::custom(RepositoryError::AlreadyExists(u.id)))?, -// None => Ok(with_status( -// json(&RepositoryResponse::of( -// db_access.create_repository(body).await?, -// )), -// StatusCode::CREATED, -// )), -// }, -// None => Err(warp::reject::custom( -// OrganizationError::OrganizationNotFound(body.organization_id), -// ))?, -// } -// } +pub async fn by_id(id: i32, db_access: impl DBRepository) -> Result { + match db_access.by_id(id)? { + None => Err(warp::reject::custom(RepositoryError::NotFound(id)))?, + Some(repository) => Ok(json(&repository)), + } +} -pub async fn get_repository_handler( +pub async fn all_handler( + db_access: impl DBRepository, + params: QueryParams, + pagination: PaginationParams, +) -> Result { + let repositories = db_access.all(params, pagination)?; + Ok(json::>(&repositories)) +} + +pub async fn create_handler( + repo: NewRepository, + db_access: impl DBRepository, +) -> Result { + match db_access.by_slug(&repo.slug)? { + Some(r) => Err(warp::reject::custom(RepositoryError::AlreadyExists(r.id))), + None => Ok(with_status( + json(&db_access.create(&repo)?), + StatusCode::CREATED, + )), + } +} +pub async fn update_handler( id: i32, + repo: UpdateRepository, db_access: impl DBRepository, ) -> Result { - match db_access.get_repository(id)? { - None => Err(warp::reject::custom(RepositoryError::NotFound(id)))?, - Some(repository) => Ok(json(&RepositoryResponse::of(repository))), + match db_access.by_id(id)? { + Some(p) => Ok(with_status( + json(&db_access.update(p.id, &repo)?), + StatusCode::OK, + )), + None => Err(warp::reject::custom(RepositoryError::NotFound(id))), } } - -// pub async fn get_repository_handler_name( -// name: String, -// db_access: impl DBRepository, -// query: GetRepositoryQuery, -// ) -> Result { -// let relations = RepositoriesRelations { -// tips: query.tips.unwrap_or_default(), -// maintainers: query.maintainers.unwrap_or_default(), -// issues: query.issues.unwrap_or_default(), -// languages: query.languages.unwrap_or_default(), -// }; -// match db_access.get_repository_by_name(&name, relations).await? { -// None => Err(warp::reject::custom(RepositoryError::NotFoundByName(name)))?, -// Some(repository) => Ok(json(&RepositoryResponse::of(repository))), -// } -// } - -pub async fn get_repositories_handler( +pub async fn delete_handler( + id: i32, db_access: impl DBRepository, - params: RepositoryQueryParams, ) -> Result { - let repositories = db_access.get_repositories(params)?; - Ok(json::>( - &repositories - .into_iter() - .map(RepositoryResponse::of) - .collect(), - )) + match db_access.by_id(id)? { + Some(p) => Ok(with_status(json(&db_access.delete(p.id)?), StatusCode::OK)), + None => Err(warp::reject::custom(RepositoryError::NotFound(id))), + } } - -// pub async fn delete_repository_handler( -// id: i32, -// db_access: impl DBRepository, -// ) -> Result { -// match db_access -// .get_repository(id, RepositoriesRelations::default()) -// .await? -// { -// Some(_) => { -// let _ = &db_access.delete_repository(id).await?; -// Ok(StatusCode::NO_CONTENT) -// } -// None => Err(warp::reject::custom(RepositoryError::NotFound(id)))?, -// } -// } diff --git a/src/api/repositories/models.rs b/src/api/repositories/models.rs index 5cd42bb..126f7bb 100644 --- a/src/api/repositories/models.rs +++ b/src/api/repositories/models.rs @@ -1,156 +1,46 @@ -use std::fmt; - -use chrono::{DateTime, Utc}; -use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable}; -use serde_derive::{Deserialize, Serialize}; -use warp::{ - http::StatusCode, - reject::Reject, - reply::{Reply, Response}, -}; - -use crate::languages::models::Language; use crate::schema::repositories; +use chrono::{DateTime, Utc}; use diesel::prelude::*; -use crate::{ - db::utils::{default_sort_direction, sort_direction}, - error_handler::ErrorResponse, -}; -use thiserror::Error; +use serde_derive::{Deserialize, Serialize}; -#[derive(Queryable, Selectable, Identifiable, Associations, Debug, PartialEq)] -#[diesel(belongs_to(Language))] +#[derive( + AsChangeset, Queryable, Identifiable, Selectable, Debug, PartialEq, Serialize, Deserialize, +)] #[diesel(table_name = repositories)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct Repository { pub id: i32, pub name: String, + pub slug: String, pub language_id: i32, - // pub url: String, - // pub icon: Option, - // pub e_tag: String, - // pub organization_id: Option, - // pub created_at: DateTime, - // pub updated_at: Option>, + pub project_id: i32, + pub created_at: DateTime, + pub updated_at: Option>, } -#[derive(Deserialize)] -pub struct RepositoryQueryParams { - pub language: Option, +#[derive(Deserialize, Debug)] +pub struct QueryParams { + pub slug: Option, + pub name: Option, + pub language_ids: Option, + pub project_ids: Option, } +#[derive(Insertable, Serialize, Deserialize, Debug)] +#[diesel(table_name = repositories)] pub struct NewRepository { pub name: String, - pub icon: String, - pub organization_id: i32, - pub url: String, - pub e_tag: String, -} - -#[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct RepositoryResponse { - pub id: i32, - pub name: String, - // pub url: String, - // pub icon: Option, - // pub e_tag: String, - // pub organization_id: Option, - // pub created_at: DateTime, - // pub updated_at: Option>, -} - -impl RepositoryResponse { - pub fn of(repository: Repository) -> RepositoryResponse { - RepositoryResponse { - id: repository.id, - name: repository.name, - // organization_id: repository.organization_id, - // icon: repository.icon, - // url: repository.url, - // e_tag: repository.e_tag, - // created_at: repository.created_at, - // updated_at: repository.updated_at, - } - } -} - -#[derive(Default)] -pub struct RepositoriesRelations { - pub issues: bool, - pub tips: bool, - pub maintainers: bool, - pub languages: bool, -} -// query args - -#[derive(Serialize, Deserialize, Default)] -pub struct GetRepositoryQuery { - pub languages: Option, - pub tips: Option, - pub maintainers: Option, - pub issues: Option, - // TODO: add filters - // pub is_maintainer: Option, - // pub has_tips: Option, - // pub has_issues: Option, - // pub has_wishes: Option, -} - -#[derive(Serialize, Deserialize)] -pub struct RepositorySort { - pub field: String, - pub order: String, -} -impl RepositorySort { - pub fn new(field: &str, descending: bool) -> Result { - if field != "id" && field != "name" { - return Err(RepositorySortError::InvalidSortBy(field.to_owned())); - } - - Ok(Self { - field: format!("repository.{field}"), - order: sort_direction(descending), - }) - } -} - -impl Default for RepositorySort { - fn default() -> Self { - RepositorySort { - field: "id".to_string(), - order: default_sort_direction(), - } - } -} - -#[derive(Clone, Error, Debug, Deserialize, PartialEq)] -pub enum RepositorySortError { - InvalidSortBy(String), -} - -impl fmt::Display for RepositorySortError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - RepositorySortError::InvalidSortBy(field) => { - write!(f, "Sort by {} is invalid", field) - } - } - } + pub slug: String, + pub language_id: i32, + pub project_id: i32, } -impl Reject for RepositorySortError {} - -impl Reply for RepositorySortError { - fn into_response(self) -> Response { - let status_code = match self { - RepositorySortError::InvalidSortBy(_) => StatusCode::BAD_REQUEST, - }; - let code = status_code; - let message = self.to_string(); - - let json = warp::reply::json(&ErrorResponse { message }); - - warp::reply::with_status(json, code).into_response() - } +#[derive(AsChangeset, Serialize, Deserialize, Debug)] +#[diesel(table_name = repositories)] +pub struct UpdateRepository { + pub name: Option, + pub slug: Option, + pub language_id: Option, + pub project_id: Option, } diff --git a/src/api/repositories/routes.rs b/src/api/repositories/routes.rs index 0983fb1..212ea83 100644 --- a/src/api/repositories/routes.rs +++ b/src/api/repositories/routes.rs @@ -4,21 +4,21 @@ use warp::filters::BoxedFilter; use warp::{Filter, Reply}; use crate::auth::with_auth; -use crate::organization::db::DBOrganization; +use crate::types::PaginationParams; use super::db::DBRepository; use super::handlers; -use super::models::RepositoryQueryParams; +use super::models::QueryParams; // use crate::pagination::GetPagination; // use crate::pagination::GetSort; fn with_db( - db_pool: impl DBRepository + DBOrganization, -) -> impl Filter + Clone { + db_pool: impl DBRepository, +) -> impl Filter + Clone { warp::any().map(move || db_pool.clone()) } -pub fn routes(db_access: impl DBRepository + DBOrganization) -> BoxedFilter<(impl Reply,)> { +pub fn routes(db_access: impl DBRepository) -> BoxedFilter<(impl Reply,)> { let repository = warp::path!("repositories"); // TODO: move this to the "organization" endpoint as a subendpoint let repository_id = warp::path!("repositories" / i32); // let repository_name = warp::path!("repositories" / "name" / String); @@ -26,40 +26,39 @@ pub fn routes(db_access: impl DBRepository + DBOrganization) -> BoxedFilter<(imp let get_repositories = repository .and(warp::get()) .and(with_db(db_access.clone())) - .and(warp::query::()) - .and_then(handlers::get_repositories_handler); - - // let get_repository_by_name = repository_name - // .and(warp::get()) - // .and(with_db(db_access.clone())) - // .and(warp::query::()) - // .and_then(handlers::get_repository_handler_name); + .and(warp::query::()) + .and(warp::query::()) + .and_then(handlers::all_handler); let get_repository = repository_id .and(warp::get()) .and(with_db(db_access.clone())) - .and_then(handlers::get_repository_handler); - - // let create_repository = repository - // .and(with_auth()) - // .and(warp::post()) - // .and(warp::body::json()) - // .and(with_db(db_access.clone())) - // .and_then(handlers::create_repository_handler); + .and_then(handlers::by_id); - // let delete_repository = repository_id - // .and(with_auth()) - // .and(warp::delete()) - // .and(with_db(db_access.clone())) - // .and_then(handlers::delete_repository_handler); + let create_repository = repository + .and(with_auth()) + .and(warp::post()) + .and(warp::body::json()) + .and(with_db(db_access.clone())) + .and_then(handlers::create_handler); - // let route = get_repositories - // .or(get_repository) - // .or(create_repository) - // .or(delete_repository) - // .or(get_repository_by_name); + let update_repository = repository_id + .and(with_auth()) + .and(warp::post()) + .and(warp::body::json()) + .and(with_db(db_access.clone())) + .and_then(handlers::update_handler); - // route.boxed() + let delete_repository = repository_id + .and(with_auth()) + .and(warp::delete()) + .and(with_db(db_access.clone())) + .and_then(handlers::delete_handler); - get_repository.or(get_repositories).boxed() + get_repositories + .or(get_repository) + .or(create_repository) + .or(delete_repository) + .or(update_repository) + .boxed() } diff --git a/src/main.rs b/src/main.rs index b89e3b3..c39f9be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,6 @@ mod db; mod errors; // mod languages; // mod issue; -// mod repository; // mod user; pub mod schema; mod utils; diff --git a/src/schema.rs b/src/schema.rs index b41eb2a..468ba7f 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,5 +1,15 @@ // @generated automatically by Diesel CLI. +diesel::table! { + languages (id) { + id -> Int4, + #[max_length = 255] + name -> Nullable, + created_at -> Timestamptz, + updated_at -> Nullable, + } +} + diesel::table! { projects (id) { id -> Int4, @@ -23,15 +33,18 @@ diesel::table! { slug -> Varchar, #[max_length = 255] name -> Varchar, - project_id -> Nullable, + language_id -> Int4, + project_id -> Int4, created_at -> Timestamptz, updated_at -> Nullable, } } +diesel::joinable!(repositories -> languages (language_id)); diesel::joinable!(repositories -> projects (project_id)); diesel::allow_tables_to_appear_in_same_query!( + languages, projects, repositories, ); diff --git a/src/utils.rs b/src/utils.rs index 82b5a8e..ac331b6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,7 +2,7 @@ use ::warp::Reply; use warp::{filters::BoxedFilter, Filter}; use crate::{ - api::{health, projects}, + api::{health, projects, repositories}, db::{ self, errors::DBError, @@ -42,17 +42,25 @@ pub async fn setup_db(url: &String) -> DBAccess { pub fn setup_filters(db: DBAccess) -> BoxedFilter<(impl Reply,)> { let health_route = health::routes::routes(db.clone()); let projects_route = projects::routes::routes(db.clone()); + let repositories_route = repositories::routes::routes(db.clone()); health_route .or(projects_route) + .or(repositories_route) .with(warp::cors().allow_any_origin()) .recover(error_handler) .boxed() } -// pub fn parse_ids(s: &str) -> Vec { -// s.split(",").map(|id| id.parse::().unwrap()).collect() // TODO: Handle errors, remove unwrap() -// } +pub fn parse_ids(s: &str) -> Vec { + let mut ids = Vec::new(); + for token in s.split_whitespace() { + if let Ok(id) = token.parse::() { + ids.push(id); + } + } + ids +} pub fn parse_comma_values(s: &str) -> Vec { s.split(",").map(|el: &str| el.to_string()).collect() From 64ca9ec2afb68adcc97a586fda3fbf07001a69c6 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 5 May 2024 20:05:59 -0300 Subject: [PATCH 26/98] refactor: users --- migrations/2024-04-13-204151_init/up.sql | 7 + src/api/mod.rs | 2 +- src/api/users/db.rs | 231 +++++++---------------- src/api/users/errors.rs | 2 +- src/api/users/handlers.rs | 149 +++------------ src/api/users/models.rs | 137 +++----------- src/api/users/routes.rs | 37 ++-- src/schema.rs | 11 ++ 8 files changed, 159 insertions(+), 417 deletions(-) diff --git a/migrations/2024-04-13-204151_init/up.sql b/migrations/2024-04-13-204151_init/up.sql index 15faf35..0a0064e 100644 --- a/migrations/2024-04-13-204151_init/up.sql +++ b/migrations/2024-04-13-204151_init/up.sql @@ -25,4 +25,11 @@ CREATE TABLE IF NOT EXISTS repositories ( project_id INT REFERENCES projects(id) NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NULL +); +-- all the users including maintainers and admins +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + username VARCHAR(100) UNIQUE NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE NULL ); \ No newline at end of file diff --git a/src/api/mod.rs b/src/api/mod.rs index 1af8945..ea0e553 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -3,4 +3,4 @@ pub mod health; // pub mod languages; pub mod projects; pub mod repositories; -// pub mod users; +pub mod users; diff --git a/src/api/users/db.rs b/src/api/users/db.rs index 760b06f..d1e6bc1 100644 --- a/src/api/users/db.rs +++ b/src/api/users/db.rs @@ -1,181 +1,92 @@ -use crate::{ - db::{ - pool::DBAccess, - utils::{ - execute_query_with_timeout, query_one_timeout, query_opt_timeout, query_with_timeout, - DB_QUERY_TIMEOUT, - }, - }, - pagination::GetPagination, -}; -use mobc::async_trait; -use mobc_postgres::tokio_postgres::Row; -use warp::reject; +use diesel::dsl::now; +use diesel::prelude::*; -use super::models::{NewUser, PatchUser, User, UserSort, UsersRelations}; +use super::models::{NewUser, UpdateUser, User}; +use crate::schema::users::dsl as users_dsl; -const TABLE: &str = "users"; +use crate::db::{ + errors::DBError, + pool::{DBAccess, DBAccessor}, +}; +use crate::types::PaginationParams; -#[async_trait] pub trait DBUser: Send + Sync + Clone + 'static { - async fn get_user( - &self, - id: i32, - relations: UsersRelations, - ) -> Result, reject::Rejection>; - async fn get_user_by_username( - &self, - username: &str, - relations: UsersRelations, - ) -> Result, reject::Rejection>; - async fn get_users( - &self, - relations: UsersRelations, - pagination: GetPagination, - sort: UserSort, - ) -> Result, reject::Rejection>; - async fn create_user(&self, user: NewUser) -> Result; - async fn update_user_maintainers( - &self, - id: i32, - user: PatchUser, - ) -> Result; - async fn delete_user(&self, id: i32) -> Result<(), reject::Rejection>; + fn by_id(&self, id: i32) -> Result, DBError>; + fn by_username(&self, username: &str) -> Result, DBError>; + fn all(&self, pagination: PaginationParams) -> Result, DBError>; + fn create(&self, user: &NewUser) -> Result; + fn update(&self, id: i32, user: &UpdateUser) -> Result; + fn delete(&self, id: i32) -> Result<(), DBError>; } -#[async_trait] impl DBUser for DBAccess { - async fn get_user( - &self, - id: i32, - relations: UsersRelations, - ) -> Result, reject::Rejection> { - let mut query = format!("SELECT * FROM {} ", TABLE); - if relations.maintainers { - query += "LEFT JOIN maintainers on maintainers.user_id = users.id "; - query += "LEFT JOIN repositories on maintainers.repository_id = repositories.id "; - } - if relations.issues { - query += "LEFT JOIN issues on issues.user_id = users.id "; - if relations.tips { - query += "LEFT JOIN tips on tips.id = issues.id "; - } - } - if relations.wishes { - query += "LEFT JOIN comments on comments.user_id = users.id "; - query += "LEFT JOIN wishes on wishes.id = comments.wish_id "; - } - query += "WHERE id = $1"; + fn by_id(&self, id: i32) -> Result, DBError> { + let conn = &mut self.get_db_conn(); - match query_opt_timeout(self, query.as_str(), &[&id], DB_QUERY_TIMEOUT).await? { - Some(user) => Ok(Some(row_to_user(&user))), - None => Ok(None), - } + let result = users_dsl::users + .find(id) + .first::(conn) + .optional() + .map_err(DBError::from)?; + + Ok(result) } - async fn get_user_by_username( - &self, - username: &str, - relations: UsersRelations, - ) -> Result, reject::Rejection> { - let mut query = format!("SELECT * FROM {} ", TABLE); - if relations.maintainers { - query += "LEFT JOIN maintainers on maintainers.user_id = users.id "; - query += "LEFT JOIN repositories on maintainers.repository_id = repositories.id "; - } - if relations.issues { - query += "LEFT JOIN issues on issues.user_id = users.id "; - if relations.tips { - query += "LEFT JOIN tips on tips.id = issues.id "; - } - } - if relations.wishes { - query += "LEFT JOIN comments on comments.user_id = users.id "; - query += "LEFT JOIN wishes on wishes.id = comments.wish_id "; - } - query += "WHERE username = $1"; - match query_opt_timeout(self, query.as_str(), &[&username], DB_QUERY_TIMEOUT).await? { - Some(user) => Ok(Some(row_to_user(&user))), - None => Ok(None), + fn by_username(&self, username: &str) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + let mut query = users_dsl::users.into_boxed(); + query = query.filter(users_dsl::username.eq(username)); + query = query.limit(1); + let result: Vec = query.load::(conn)?; + if result.is_empty() { + Ok(None) + } else { + Ok(Some(User { + id: result[0].id, + username: result[0].username.clone(), + created_at: result[0].created_at, + updated_at: result[0].updated_at, + })) } } - async fn get_users( - &self, - relations: UsersRelations, - pagination: GetPagination, - sort: UserSort, - ) -> Result, reject::Rejection> { - let mut query = format!("SELECT * FROM {} ", TABLE); - if relations.maintainers { - query += "LEFT JOIN maintainers on maintainers.user_id = users.id "; - query += "LEFT JOIN repositories on maintainers.repository_id = repositories.id "; - } - if relations.issues { - query += "LEFT JOIN issues on issues.user_id = users.id "; - if relations.tips { - query += "LEFT JOIN tips on tips.id = issues.id "; - } - } - if relations.wishes { - query += "LEFT JOIN comments on comments.user_id = users.id "; - query += "LEFT JOIN wishes on wishes.id = comments.wish_id "; - } + fn all(&self, pagination: PaginationParams) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + let mut query = users_dsl::users.into_boxed(); - query += &format!("ORDER BY {} {}", sort.field, sort.order); // cannot use $1 or $2 - - query += "LIMIT $1 OFFSET $2"; - let rows = query_with_timeout( - self, - query.as_str(), - &[&pagination.limit, &pagination.offset], - DB_QUERY_TIMEOUT, - ) - .await?; - Ok(rows.iter().map(row_to_user).collect()) - } + query = query.offset(pagination.offset).limit(pagination.limit); - async fn create_user(&self, user: NewUser) -> Result { - let query = format!("INSERT INTO {} (username) VALUES ($1) RETURNING *", TABLE); - let row = query_one_timeout(self, &query, &[&user.username], DB_QUERY_TIMEOUT).await?; - let new_user = row_to_user(&row); - - if let Some(repositories) = user.repositories { - for repo_id in repositories { - let query = - "INSERT INTO maintainers (user_id, repository_id) VALUES ($1, $2)".to_string(); - query_one_timeout(self, &query, &[&new_user.id, &repo_id], DB_QUERY_TIMEOUT) - .await?; - } - } - Ok(new_user) + let result = query.load::(conn)?; + Ok(result) } - async fn update_user_maintainers( - &self, - id: i32, - user: PatchUser, - ) -> Result { - let query = "DELETE maintainers WHERE user_ID = $1".to_string(); - let row = query_one_timeout(self, &query, &[&id], DB_QUERY_TIMEOUT).await?; - for repo_id in user.repositories { - let query = - "INSERT INTO maintainers (user_id, repository_id) VALUES ($1, $2)".to_string(); - query_one_timeout(self, &query, &[&id, &repo_id], DB_QUERY_TIMEOUT).await?; - } - // TODO: make a db tx - Ok(row_to_user(&row)) + + fn create(&self, user: &NewUser) -> Result { + let conn = &mut self.get_db_conn(); + + let user = diesel::insert_into(users_dsl::users) + .values(user) + .get_result(conn) + .map_err(DBError::from)?; + + Ok(user) } - async fn delete_user(&self, id: i32) -> Result<(), reject::Rejection> { - let query = format!("DELETE FROM {} WHERE id = $1", TABLE); - execute_query_with_timeout(self, &query, &[&id], DB_QUERY_TIMEOUT).await + fn update(&self, id: i32, form: &UpdateUser) -> Result { + let conn = &mut self.get_db_conn(); + + let user = diesel::update(users_dsl::users.filter(users_dsl::id.eq(id))) + .set((form, users_dsl::updated_at.eq(now))) + .get_result::(conn) + .map_err(DBError::from)?; + + Ok(user) } -} -fn row_to_user(row: &Row) -> User { - let id: i32 = row.get(0); - let username: &str = row.get(1); - User { - id, - username: username.to_string(), + fn delete(&self, id: i32) -> Result<(), DBError> { + let conn = &mut self.get_db_conn(); + diesel::delete(users_dsl::users.filter(users_dsl::id.eq(id))) + .execute(conn) + .map_err(DBError::from)?; + + Ok(()) } } diff --git a/src/api/users/errors.rs b/src/api/users/errors.rs index f468316..2e32d83 100644 --- a/src/api/users/errors.rs +++ b/src/api/users/errors.rs @@ -8,7 +8,7 @@ use warp::{ reply::{Reply, Response}, }; -use crate::error_handler::ErrorResponse; +use crate::errors::ErrorResponse; #[derive(Clone, Error, Debug, Deserialize, PartialEq)] pub enum UserError { diff --git a/src/api/users/handlers.rs b/src/api/users/handlers.rs index 5fad12b..698eca0 100644 --- a/src/api/users/handlers.rs +++ b/src/api/users/handlers.rs @@ -4,144 +4,57 @@ use warp::{ reply::{json, with_status, Reply}, }; -use crate::{ - pagination::{GetPagination, GetSort}, - repository::{db::DBRepository, models::RepositoriesRelations}, -}; +use crate::types::PaginationParams; use super::{ db::DBUser, errors::UserError, - models::{GetUserQuery, NewUser, PatchUser, UserResponse, UserSort, UsersRelations}, + models::{NewUser, UpdateUser}, }; -pub async fn create_user_handler( - body: NewUser, - db_access: impl DBUser + DBRepository, -) -> Result { - match db_access - .get_user_by_username(&body.username, UsersRelations::default()) - .await? - { - Some(u) => Err(warp::reject::custom(UserError::AlreadyExists(u.id)))?, - None => { - if let Some(repositories) = body.repositories.clone() { - for repo_id in repositories { - if db_access - .get_repository(repo_id, RepositoriesRelations::default()) - .await? - .is_none() - { - return Err(warp::reject::custom(UserError::CannotBeCreated(format!( - "repository {repo_id} does not exist" - )))); - } - } - } - let new_user = db_access.create_user(body).await?; - Ok(with_status( - json(&UserResponse::of(new_user)), - StatusCode::CREATED, - )) - } - } -} -pub async fn patch_user_handler( - id: i32, - body: PatchUser, - db_access: impl DBUser + DBRepository, -) -> Result { - match db_access.get_user(id, UsersRelations::default()).await? { +pub async fn by_id(id: i32, db_access: impl DBUser) -> Result { + match db_access.by_id(id)? { None => Err(warp::reject::custom(UserError::NotFound(id)))?, - Some(_) => { - for repo_id in &body.repositories { - if db_access - .get_repository(*repo_id, RepositoriesRelations::default()) - .await? - .is_none() - { - return Err(warp::reject::custom(UserError::CannotBeUpdated( - id, - format!("repository {repo_id} does not exist"), - ))); - } - } - - db_access.update_user_maintainers(id, body).await?; - Ok(StatusCode::NO_CONTENT) - } + Some(user) => Ok(json(&user)), } } -pub async fn get_user_handler( - id: i32, +pub async fn all_handler( db_access: impl DBUser, - query: GetUserQuery, + pagination: PaginationParams, ) -> Result { - let relations = UsersRelations { - wishes: query.wishes.unwrap_or_default(), - tips: query.tips.unwrap_or_default(), - maintainers: query.maintainers.unwrap_or_default(), - issues: query.issues.unwrap_or_default(), - }; - - match db_access.get_user(id, relations).await? { - None => Err(warp::reject::custom(UserError::NotFound(id)))?, - Some(user) => Ok(json(&UserResponse::of(user))), - } + let users = db_access.all(pagination)?; + Ok(json::>(&users)) } -pub async fn get_user_by_name_handler( - name: String, +pub async fn create_handler( + user: NewUser, db_access: impl DBUser, - query: GetUserQuery, ) -> Result { - let relations = UsersRelations { - wishes: query.wishes.unwrap_or_default(), - tips: query.tips.unwrap_or_default(), - maintainers: query.maintainers.unwrap_or_default(), - issues: query.issues.unwrap_or_default(), - }; - - match db_access.get_user_by_username(&name, relations).await? { - None => Err(warp::reject::custom(UserError::NotFoundByName(name)))?, - Some(user) => Ok(json(&UserResponse::of(user))), + match db_access.by_username(&user.username)? { + Some(r) => Err(warp::reject::custom(UserError::AlreadyExists(r.id))), + None => Ok(with_status( + json(&db_access.create(&user)?), + StatusCode::CREATED, + )), } } - -pub async fn get_users_handler( +pub async fn update_handler( + id: i32, + repo: UpdateUser, db_access: impl DBUser, - query: GetUserQuery, - filters: GetPagination, - sort: GetSort, ) -> Result { - let relations = UsersRelations { - wishes: query.wishes.unwrap_or_default(), - tips: query.tips.unwrap_or_default(), - maintainers: query.maintainers.unwrap_or_default(), - issues: query.issues.unwrap_or_default(), - }; - let pagination = filters.validate()?; - let sort = sort.validate()?; - let user_sort = match (sort.sort_by, sort.descending) { - (Some(sort_by), Some(descending)) => UserSort::new(&sort_by, descending)?, - _ => UserSort::default(), - }; - - let users = db_access - .get_users(relations, pagination, user_sort) - .await?; - Ok(json::>( - &users.into_iter().map(UserResponse::of).collect(), - )) + match db_access.by_id(id)? { + Some(p) => Ok(with_status( + json(&db_access.update(p.id, &repo)?), + StatusCode::OK, + )), + None => Err(warp::reject::custom(UserError::NotFound(id))), + } } - -pub async fn delete_user_handler(id: i32, db_access: impl DBUser) -> Result { - match db_access.get_user(id, UsersRelations::default()).await? { - Some(_) => { - let _ = &db_access.delete_user(id).await?; - Ok(StatusCode::NO_CONTENT) - } - None => Err(warp::reject::custom(UserError::NotFound(id)))?, +pub async fn delete_handler(id: i32, db_access: impl DBUser) -> Result { + match db_access.by_id(id)? { + Some(p) => Ok(with_status(json(&db_access.delete(p.id)?), StatusCode::OK)), + None => Err(warp::reject::custom(UserError::NotFound(id))), } } diff --git a/src/api/users/models.rs b/src/api/users/models.rs index f863383..482178a 100644 --- a/src/api/users/models.rs +++ b/src/api/users/models.rs @@ -1,126 +1,37 @@ -use std::fmt::{self}; +use crate::schema::users; +use chrono::{DateTime, Utc}; +use diesel::prelude::*; use serde_derive::{Deserialize, Serialize}; -use thiserror::Error; -use warp::{ - http::StatusCode, - reject::Reject, - reply::{Reply, Response}, -}; -use crate::{ - db::utils::{default_sort_direction, sort_direction}, - error_handler::ErrorResponse, -}; - -#[derive(Deserialize)] +#[derive( + AsChangeset, + Queryable, + Identifiable, + Selectable, + Debug, + PartialEq, + Serialize, + Deserialize, + Clone, +)] +#[diesel(table_name = users)] +#[diesel(check_for_backend(diesel::pg::Pg))] pub struct User { pub id: i32, pub username: String, - // TODO: add optional fields + pub created_at: DateTime, + pub updated_at: Option>, } -#[derive(Serialize, Deserialize)] +#[derive(Insertable, Serialize, Deserialize, Debug)] +#[diesel(table_name = users)] pub struct NewUser { pub username: String, - pub repositories: Option>, -} -#[derive(Serialize, Deserialize)] -pub struct PatchUser { - pub repositories: Vec, } -#[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct UserResponse { - pub id: i32, - pub username: String, -} - -impl UserResponse { - pub fn of(user: User) -> UserResponse { - UserResponse { - id: user.id, - username: user.username, - } - } -} - -#[derive(Default)] -pub struct UsersRelations { - pub wishes: bool, - pub tips: bool, - pub maintainers: bool, - pub issues: bool, -} -// query args - -#[derive(Serialize, Deserialize, Default)] -pub struct GetUserQuery { - pub wishes: Option, - pub tips: Option, - pub maintainers: Option, - pub issues: Option, - // TODO: add filters - // pub is_maintainer: Option, - // pub has_tips: Option, - // pub has_issues: Option, - // pub has_wishes: Option, -} - -#[derive(Serialize, Deserialize)] -pub struct UserSort { - pub field: String, - pub order: String, -} -impl UserSort { - pub fn new(field: &str, descending: bool) -> Result { - if field != "id" && field != "username" { - return Err(UserSortError::InvalidSortBy(field.to_owned())); - } - - Ok(Self { - field: format!("users.{field}"), - order: sort_direction(descending), - }) - } -} - -impl Default for UserSort { - fn default() -> Self { - UserSort { - field: "id".to_string(), - order: default_sort_direction(), - } - } -} - -#[derive(Clone, Error, Debug, Deserialize, PartialEq)] -pub enum UserSortError { - InvalidSortBy(String), -} - -impl fmt::Display for UserSortError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - UserSortError::InvalidSortBy(field) => { - write!(f, "Sort by {} is invalid", field) - } - } - } -} - -impl Reject for UserSortError {} - -impl Reply for UserSortError { - fn into_response(self) -> Response { - let status_code = match self { - UserSortError::InvalidSortBy(_) => StatusCode::BAD_REQUEST, - }; - let code = status_code; - let message = self.to_string(); - - let json = warp::reply::json(&ErrorResponse { message }); - - warp::reply::with_status(json, code).into_response() - } +#[derive(AsChangeset, Serialize, Deserialize, Debug)] +#[diesel(table_name = users)] +pub struct UpdateUser { + pub username: Option, } diff --git a/src/api/users/routes.rs b/src/api/users/routes.rs index bda9146..926b3ba 100644 --- a/src/api/users/routes.rs +++ b/src/api/users/routes.rs @@ -4,20 +4,18 @@ use warp::filters::BoxedFilter; use warp::{Filter, Reply}; use crate::auth::with_auth; -use crate::pagination::{GetPagination, GetSort}; -use crate::repository::db::DBRepository; +use crate::types::PaginationParams; use super::db::DBUser; use super::handlers; -use super::models::GetUserQuery; fn with_db( - db_pool: impl DBUser + DBRepository, -) -> impl Filter + Clone { + db_pool: impl DBUser, +) -> impl Filter + Clone { warp::any().map(move || db_pool.clone()) } -pub fn routes(db_access: impl DBUser + DBRepository) -> BoxedFilter<(impl Reply,)> { +pub fn routes(db_access: impl DBUser) -> BoxedFilter<(impl Reply,)> { let user = warp::path!("users"); let user_id = warp::path!("users" / i32); let user_name = warp::path!("users" / "username" / String); @@ -26,48 +24,39 @@ pub fn routes(db_access: impl DBUser + DBRepository) -> BoxedFilter<(impl Reply, let get_users = user .and(warp::get()) .and(with_db(db_access.clone())) - .and(warp::query::()) - .and(warp::query::()) - .and(warp::query::()) - .and_then(handlers::get_users_handler); + .and(warp::query::()) + .and_then(handlers::all_handler); let get_user = user_id .and(warp::get()) .and(with_db(db_access.clone())) - .and(warp::query::()) - .and_then(handlers::get_user_handler); - - let get_user_by_name = user_name - .and(warp::get()) - .and(with_db(db_access.clone())) - .and(warp::query::()) - .and_then(handlers::get_user_by_name_handler); + .and_then(handlers::by_id); let create_user = user .and(with_auth()) .and(warp::post()) .and(warp::body::json()) .and(with_db(db_access.clone())) - .and_then(handlers::create_user_handler); - let patch_user = user_maintainer + .and_then(handlers::create_handler); + + let update_user = user_maintainer .and(with_auth()) .and(warp::patch()) .and(warp::body::json()) .and(with_db(db_access.clone())) - .and_then(handlers::patch_user_handler); + .and_then(handlers::update_handler); let delete_user = user_id .and(with_auth()) .and(warp::delete()) .and(with_db(db_access.clone())) - .and_then(handlers::delete_user_handler); + .and_then(handlers::delete_handler); let route = get_users .or(create_user) .or(get_user) .or(delete_user) - .or(get_user_by_name) - .or(patch_user); + .or(update_user); route.boxed() } diff --git a/src/schema.rs b/src/schema.rs index 468ba7f..53b6f5b 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -40,6 +40,16 @@ diesel::table! { } } +diesel::table! { + users (id) { + id -> Int4, + #[max_length = 100] + username -> Varchar, + created_at -> Timestamptz, + updated_at -> Nullable, + } +} + diesel::joinable!(repositories -> languages (language_id)); diesel::joinable!(repositories -> projects (project_id)); @@ -47,4 +57,5 @@ diesel::allow_tables_to_appear_in_same_query!( languages, projects, repositories, + users, ); From ef8e235bf155293fcde0a009c632c0878f190e76 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 12 May 2024 19:59:08 -0300 Subject: [PATCH 27/98] feat/issues --- migrations/2024-04-13-204151_init/down.sql | 4 +- migrations/2024-04-13-204151_init/up.sql | 14 +++ src/api/issue/db.rs | 76 --------------- src/api/issue/handlers.rs | 60 ------------ src/api/issue/models.rs | 44 --------- src/api/issue/routes.rs | 52 ----------- src/api/issue/utils.rs | 27 ------ src/api/issues/db.rs | 102 +++++++++++++++++++++ src/api/{issue => issues}/errors.rs | 26 +++--- src/api/issues/handlers.rs | 74 +++++++++++++++ src/api/{issue => issues}/mod.rs | 1 - src/api/issues/models.rs | 60 ++++++++++++ src/api/issues/routes.rs | 63 +++++++++++++ src/api/mod.rs | 2 +- src/api/repositories/handlers.rs | 1 + src/api/repositories/routes.rs | 2 +- src/api/users/routes.rs | 1 - src/main.rs | 3 - src/schema.rs | 21 +++++ src/utils.rs | 4 +- 20 files changed, 356 insertions(+), 281 deletions(-) delete mode 100644 src/api/issue/db.rs delete mode 100644 src/api/issue/handlers.rs delete mode 100644 src/api/issue/models.rs delete mode 100644 src/api/issue/routes.rs delete mode 100644 src/api/issue/utils.rs create mode 100644 src/api/issues/db.rs rename src/api/{issue => issues}/errors.rs (54%) create mode 100644 src/api/issues/handlers.rs rename src/api/{issue => issues}/mod.rs (83%) create mode 100644 src/api/issues/models.rs create mode 100644 src/api/issues/routes.rs diff --git a/migrations/2024-04-13-204151_init/down.sql b/migrations/2024-04-13-204151_init/down.sql index ab870b3..79b2107 100644 --- a/migrations/2024-04-13-204151_init/down.sql +++ b/migrations/2024-04-13-204151_init/down.sql @@ -1,3 +1,5 @@ -- Drop tables DROP TABLE IF EXISTS projects; -DROP TABLE IF EXISTS repositories; \ No newline at end of file +DROP TABLE IF EXISTS repositories; +DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS issues; \ No newline at end of file diff --git a/migrations/2024-04-13-204151_init/up.sql b/migrations/2024-04-13-204151_init/up.sql index 0a0064e..9480e8a 100644 --- a/migrations/2024-04-13-204151_init/up.sql +++ b/migrations/2024-04-13-204151_init/up.sql @@ -32,4 +32,18 @@ CREATE TABLE IF NOT EXISTS users ( username VARCHAR(100) UNIQUE NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NULL +); +-- issues +CREATE TABLE IF NOT EXISTS issues ( + id SERIAL PRIMARY KEY, + number int NOT NULL, + title VARCHAR(100) NOT NULL, + labels TEXT [], + open boolean DEFAULT true NOT NULL, + assignee_id INT REFERENCES users(id) NULL, + e_tag VARCHAR(100) NOT NULL, + repository_id INT REFERENCES repositories(id) NOT NULL, + issue_created_at TIMESTAMP WITH TIME ZONE NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE NULL ); \ No newline at end of file diff --git a/src/api/issue/db.rs b/src/api/issue/db.rs deleted file mode 100644 index 64bd6ba..0000000 --- a/src/api/issue/db.rs +++ /dev/null @@ -1,76 +0,0 @@ -use mobc::async_trait; -use mobc_postgres::tokio_postgres::Row; -use warp::reject; - -use crate::db::{ - pool::DBAccess, - utils::{ - execute_query_with_timeout, query_one_timeout, query_opt_timeout, query_with_timeout, - DB_QUERY_TIMEOUT, - }, -}; - -use super::models::{Issue, IssueCreate}; - -const TABLE: &str = "issues"; - -#[async_trait] -pub trait DBIssue: Send + Sync + Clone + 'static { - async fn get_issue(&self, id: i32) -> Result, reject::Rejection>; - async fn get_issue_by_url(&self, url: &str) -> Result, reject::Rejection>; - async fn get_issues(&self) -> Result, reject::Rejection>; - async fn create_issue(&self, issue: IssueCreate) -> Result; - async fn delete_issue(&self, id: i32) -> Result<(), reject::Rejection>; -} - -#[async_trait] -impl DBIssue for DBAccess { - async fn get_issue(&self, id: i32) -> Result, reject::Rejection> { - let query = format!("SELECT * FROM {} WHERE id = $1", TABLE); - match query_opt_timeout(self, query.as_str(), &[&id], DB_QUERY_TIMEOUT).await? { - Some(user) => Ok(Some(row_to_issue(&user))), - None => Ok(None), - } - } - - async fn get_issue_by_url(&self, url: &str) -> Result, reject::Rejection> { - let query = format!("SELECT * FROM {} WHERE url = $1", TABLE); - match query_opt_timeout(self, query.as_str(), &[&url], DB_QUERY_TIMEOUT).await? { - Some(user) => Ok(Some(row_to_issue(&user))), - None => Ok(None), - } - } - - async fn get_issues(&self) -> Result, reject::Rejection> { - let query = format!("SELECT * FROM {} ORDER BY created_at DESC", TABLE); - let rows = query_with_timeout(self, query.as_str(), &[], DB_QUERY_TIMEOUT).await?; - Ok(rows.iter().map(row_to_issue).collect()) - } - - async fn create_issue(&self, issue: IssueCreate) -> Result { - let query = format!( - "INSERT INTO {} (issue_number, repository_id, url) VALUES ($1, $2, $3) RETURNING *", - TABLE - ); - let row = query_one_timeout( - self, - &query, - &[&issue.id, &issue.repository_id, &issue.url], - DB_QUERY_TIMEOUT, - ) - .await?; - Ok(row_to_issue(&row)) - } - - async fn delete_issue(&self, id: i32) -> Result<(), reject::Rejection> { - let query = format!("DELETE FROM {} WHERE id = $1", TABLE); - execute_query_with_timeout(self, &query, &[&id], DB_QUERY_TIMEOUT).await - } -} - -fn row_to_issue(row: &Row) -> Issue { - let id: i32 = row.get(0); - let repository_id: i32 = row.get(1); - // let url: &str = row.get(2); //TODO: define if we need it in the response - Issue { id, repository_id } -} diff --git a/src/api/issue/handlers.rs b/src/api/issue/handlers.rs deleted file mode 100644 index 641736f..0000000 --- a/src/api/issue/handlers.rs +++ /dev/null @@ -1,60 +0,0 @@ -use warp::{ - http::StatusCode, - reject::Rejection, - reply::{json, Reply}, -}; - -use crate::{organization::db::DBOrganization, repository::db::DBRepository}; - -use super::{ - db::DBIssue, - errors::IssueError, - models::{IssueCreateRequest, IssueResponse}, - utils::parse_github_issue_url, -}; - -pub async fn create_issue_handler( - body: IssueCreateRequest, - db_access: impl DBIssue + DBOrganization + DBRepository, -) -> Result { - match db_access.get_issue_by_url(&body.url).await? { - Some(u) => Err(warp::reject::custom(IssueError::IssueExists(u.id)))?, - None => { - _ = parse_github_issue_url(&body.url)?; - // TODO: get or create both - // db_access.create_organization(info.organization); - // db_access.create_repository(info.repository); - // Ok(json(&IssueResponse::of( - // db_access.create_issue(body).await?, - // ))) - Ok(StatusCode::OK) //TODO: added to avoid errors in IDE - } - } -} - -pub async fn get_issue_handler(id: i32, db_access: impl DBIssue) -> Result { - match db_access.get_issue(id).await? { - None => Err(warp::reject::custom(IssueError::IssueNotFound(id)))?, - Some(issue) => Ok(json(&IssueResponse::of(issue))), - } -} - -pub async fn get_issues_handler(db_access: impl DBIssue) -> Result { - let issues = db_access.get_issues().await?; - Ok(json::>( - &issues.into_iter().map(IssueResponse::of).collect(), - )) -} - -pub async fn delete_issue_handler( - id: i32, - db_access: impl DBIssue, -) -> Result { - match db_access.get_issue(id).await? { - Some(_) => { - let _ = &db_access.delete_issue(id).await?; - Ok(StatusCode::OK) - } - None => Err(warp::reject::custom(IssueError::IssueNotFound(id)))?, - } -} diff --git a/src/api/issue/models.rs b/src/api/issue/models.rs deleted file mode 100644 index b93a455..0000000 --- a/src/api/issue/models.rs +++ /dev/null @@ -1,44 +0,0 @@ -use serde_derive::{Deserialize, Serialize}; - -#[derive(Deserialize)] -pub struct Issue { - pub id: i32, - pub repository_id: i32, -} - -#[derive(Serialize, Deserialize)] -pub struct IssueCreateRequest { - pub url: String, -} -#[derive(Serialize, Deserialize)] -pub struct IssueGetRequest { - pub id: i32, -} - -#[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct IssueResponse { - pub id: i32, - pub repository_id: i32, - // TODO: add tip -} - -impl IssueResponse { - pub fn of(issue: Issue) -> IssueResponse { - IssueResponse { - id: issue.id, - repository_id: issue.repository_id, - } - } -} - -pub struct IssueInfo { - pub organization: String, - pub repository: String, - pub issue_id: u32, -} - -pub struct IssueCreate { - pub repository_id: u32, - pub id: u32, - pub url: String, -} diff --git a/src/api/issue/routes.rs b/src/api/issue/routes.rs deleted file mode 100644 index 14d9913..0000000 --- a/src/api/issue/routes.rs +++ /dev/null @@ -1,52 +0,0 @@ -use std::convert::Infallible; - -use warp::filters::BoxedFilter; -use warp::{Filter, Reply}; - -use crate::auth::with_auth; -use crate::organization::db::DBOrganization; -use crate::repository::db::DBRepository; - -use super::db::DBIssue; -use super::handlers; - -fn with_db( - db_pool: impl DBIssue + DBOrganization + DBRepository, -) -> impl Filter + Clone -{ - warp::any().map(move || db_pool.clone()) -} - -pub fn routes( - db_access: impl DBIssue + DBOrganization + DBRepository, -) -> BoxedFilter<(impl Reply,)> { - let issue = warp::path!("issues"); - let issue_id = warp::path!("issues" / i32); - - let get_issues = issue - .and(warp::get()) - .and(with_db(db_access.clone())) - .and_then(handlers::get_issues_handler); - - let get_issue = issue_id - .and(warp::get()) - .and(with_db(db_access.clone())) - .and_then(handlers::get_issue_handler); - - let create_issue = issue - .and(with_auth()) - .and(warp::post()) - .and(warp::body::json()) - .and(with_db(db_access.clone())) - .and_then(handlers::create_issue_handler); - - let delete_issue = issue_id - .and(with_auth()) - .and(warp::delete()) - .and(with_db(db_access.clone())) - .and_then(handlers::delete_issue_handler); - - let route = get_issues.or(get_issue).or(create_issue).or(delete_issue); - - route.boxed() -} diff --git a/src/api/issue/utils.rs b/src/api/issue/utils.rs deleted file mode 100644 index fc00ab6..0000000 --- a/src/api/issue/utils.rs +++ /dev/null @@ -1,27 +0,0 @@ -use super::{errors::IssueError, models::IssueInfo}; -use url::Url; - -pub fn parse_github_issue_url(url_str: &str) -> Result { - let url = Url::parse(url_str).map_err(|_| IssueError::IssueInvalidURL)?; - let path_segments: Vec<&str> = url - .path_segments() - .ok_or(IssueError::IssueInvalidURL)? - .collect(); - if path_segments.len() >= 4 && path_segments[0] == "github.com" && path_segments[2] == "issues" - { - // Extract organization, repository, and issue id - let organization = path_segments[1].to_owned(); - let repository = path_segments[2].to_owned(); - let issue_id = path_segments[3] - .parse::() - .map_err(|_| IssueError::IssueInvalidURL)?; - - Ok(IssueInfo { - organization, - repository, - issue_id, - }) - } else { - Err(IssueError::IssueInvalidURL) - } -} diff --git a/src/api/issues/db.rs b/src/api/issues/db.rs new file mode 100644 index 0000000..c2f8bfc --- /dev/null +++ b/src/api/issues/db.rs @@ -0,0 +1,102 @@ +use diesel::dsl::now; +use diesel::prelude::*; + +use super::models::{Issue, NewIssue, QueryParams, UpdateIssue}; +use crate::schema::issues::dsl as issues_dsl; + +use crate::db::{ + errors::DBError, + pool::{DBAccess, DBAccessor}, +}; +use crate::types::PaginationParams; +use crate::utils; +pub trait DBIssue: Send + Sync + Clone + 'static { + fn all(&self, params: QueryParams, pagination: PaginationParams) + -> Result, DBError>; + fn by_id(&self, id: i32) -> Result, DBError>; + fn by_number(&self, repository_id: i32, number: i32) -> Result, DBError>; + fn create(&self, issue: &NewIssue) -> Result; + fn update(&self, id: i32, issue: &UpdateIssue) -> Result; + fn delete(&self, id: i32) -> Result<(), DBError>; +} + +impl DBIssue for DBAccess { + fn all( + &self, + params: QueryParams, + pagination: PaginationParams, + ) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + let mut query = issues_dsl::issues.into_boxed(); + + if let Some(raw_labels) = params.labels { + let labels: Vec = utils::parse_comma_values(&raw_labels); + query = query.filter(issues_dsl::labels.overlaps_with(labels)); + } + + if let Some(open) = params.open { + query = query.filter(issues_dsl::open.eq(open)); + } + + if let Some(assignee_id) = params.assignee_id { + query = query.filter(issues_dsl::assignee_id.eq(assignee_id)); + } + + if let Some(repository_id) = params.repository_id { + query = query.filter(issues_dsl::repository_id.eq(repository_id)); + } + + query = query.offset(pagination.offset).limit(pagination.limit); + + let result = query.load::(conn)?; + Ok(result) + } + fn by_id(&self, id: i32) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + let result = issues_dsl::issues + .find(id) + .first::(conn) + .optional() + .map_err(DBError::from)?; + Ok(result) + } + fn by_number(&self, repository_id: i32, number: i32) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + let result = issues_dsl::issues + .filter(issues_dsl::repository_id.eq(repository_id)) + .filter(issues_dsl::number.eq(number)) + .first::(conn) + .optional() + .map_err(DBError::from)?; + Ok(result) + } + fn create(&self, form: &NewIssue) -> Result { + let conn = &mut self.get_db_conn(); + let project = diesel::insert_into(issues_dsl::issues) + .values(form) + .get_result(conn) + .map_err(DBError::from)?; + + Ok(project) + } + + fn update(&self, id: i32, issue: &UpdateIssue) -> Result { + let conn = &mut self.get_db_conn(); + + let project = diesel::update(issues_dsl::issues.filter(issues_dsl::id.eq(id))) + .set((issue, issues_dsl::updated_at.eq(now))) + .get_result::(conn) + .map_err(DBError::from)?; + + Ok(project) + } + + fn delete(&self, id: i32) -> Result<(), DBError> { + let conn = &mut self.get_db_conn(); + diesel::delete(issues_dsl::issues.filter(issues_dsl::id.eq(id))) + .execute(conn) + .map_err(DBError::from)?; + + Ok(()) + } +} diff --git a/src/api/issue/errors.rs b/src/api/issues/errors.rs similarity index 54% rename from src/api/issue/errors.rs rename to src/api/issues/errors.rs index 92591f8..ca8a140 100644 --- a/src/api/issue/errors.rs +++ b/src/api/issues/errors.rs @@ -8,26 +8,26 @@ use warp::{ reply::{Reply, Response}, }; -use crate::error_handler::ErrorResponse; +use crate::errors::ErrorResponse; #[derive(Clone, Error, Debug, Deserialize, PartialEq)] pub enum IssueError { - IssueExists(i32), - IssueNotFound(i32), - IssueInvalidURL, + AlreadyExists(i32), + NotFound(i32), + InvalidPayload, } impl fmt::Display for IssueError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - IssueError::IssueExists(id) => { - write!(f, "Issue #{} already exists", id) + IssueError::AlreadyExists(id) => { + write!(f, "Issue #{id} already exists") } - IssueError::IssueNotFound(id) => { - write!(f, "Issue #{} not found", id) + IssueError::NotFound(id) => { + write!(f, "Issue #{id} not found") } - IssueError::IssueInvalidURL => { - write!(f, "Issue url is invalid") + IssueError::InvalidPayload => { + write!(f, "Invalid payload") } } } @@ -38,9 +38,9 @@ impl Reject for IssueError {} impl Reply for IssueError { fn into_response(self) -> Response { let code = match self { - IssueError::IssueExists(_) => StatusCode::BAD_REQUEST, - IssueError::IssueNotFound(_) => StatusCode::NOT_FOUND, - IssueError::IssueInvalidURL => StatusCode::BAD_REQUEST, + IssueError::AlreadyExists(_) => StatusCode::BAD_REQUEST, + IssueError::NotFound(_) => StatusCode::NOT_FOUND, + IssueError::InvalidPayload => StatusCode::UNPROCESSABLE_ENTITY, }; let message = self.to_string(); diff --git a/src/api/issues/handlers.rs b/src/api/issues/handlers.rs new file mode 100644 index 0000000..afc6a55 --- /dev/null +++ b/src/api/issues/handlers.rs @@ -0,0 +1,74 @@ +use warp::{ + http::StatusCode, + reject::Rejection, + reply::{json, with_status, Reply}, +}; + +use crate::{api::repositories::db::DBRepository, types::PaginationParams}; + +use super::{ + db::DBIssue, + errors::IssueError, + models::{NewIssue, QueryParams, UpdateIssue}, +}; + +pub async fn by_id(id: i32, db_access: impl DBIssue) -> Result { + match db_access.by_id(id)? { + None => Err(warp::reject::custom(IssueError::NotFound(id)))?, + Some(repository) => Ok(json(&repository)), + } +} + +pub async fn all_handler( + db_access: impl DBIssue, + params: QueryParams, + pagination: PaginationParams, +) -> Result { + let issues = db_access.all(params, pagination)?; + Ok(json::>(&issues)) +} + +pub async fn create_handler( + issue: NewIssue, + db_access: impl DBIssue + DBRepository, +) -> Result { + match DBRepository::by_id(&db_access, issue.repository_id) { + Ok(_) => match db_access.by_number(issue.repository_id, issue.number)? { + Some(r) => Err(warp::reject::custom(IssueError::AlreadyExists(r.id))), + None => Ok(with_status( + // check if repository exists + json(&DBIssue::create(&db_access, &issue)?), + StatusCode::CREATED, + )), + }, + Err(_) => Err(warp::reject::custom(IssueError::InvalidPayload)), + } +} +pub async fn update_handler( + id: i32, + issue: UpdateIssue, + db_access: impl DBIssue + DBRepository, +) -> Result { + if let Some(repo_id) = issue.repository_id { + if DBRepository::by_id(&db_access, repo_id).is_err() { + return Err(warp::reject::custom(IssueError::InvalidPayload)); + } + } + + match DBIssue::by_id(&db_access, id)? { + Some(p) => Ok(with_status( + json(&DBIssue::update(&db_access, p.id, &issue)?), + StatusCode::OK, + )), + None => Err(warp::reject::custom(IssueError::NotFound(id))), + } +} +pub async fn delete_handler(id: i32, db_access: impl DBIssue) -> Result { + match DBIssue::by_id(&db_access, id)? { + Some(_) => { + let _ = &db_access.delete(id)?; + Ok(StatusCode::OK) + } + None => Err(warp::reject::custom(IssueError::NotFound(id)))?, + } +} diff --git a/src/api/issue/mod.rs b/src/api/issues/mod.rs similarity index 83% rename from src/api/issue/mod.rs rename to src/api/issues/mod.rs index b6041a3..93246e5 100644 --- a/src/api/issue/mod.rs +++ b/src/api/issues/mod.rs @@ -3,4 +3,3 @@ pub mod errors; pub mod handlers; pub mod models; pub mod routes; -pub mod utils; diff --git a/src/api/issues/models.rs b/src/api/issues/models.rs new file mode 100644 index 0000000..565ff84 --- /dev/null +++ b/src/api/issues/models.rs @@ -0,0 +1,60 @@ +use crate::schema::issues; +use chrono::{DateTime, Utc}; +use diesel::prelude::*; + +use serde_derive::{Deserialize, Serialize}; + +#[derive( + AsChangeset, Queryable, Identifiable, Selectable, Debug, PartialEq, Serialize, Deserialize, +)] +#[diesel(table_name = issues)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Issue { + pub id: i32, + pub number: i32, + // pub title: String, + // pub labels: Option>>, + // pub open: bool, + // pub repository_id: i32, + // pub assignee_id: Option, + // pub e_tag: String, + // pub issue_created_at: DateTime, + // pub created_at: DateTime, + // pub updated_at: Option>, +} + +#[derive(Insertable, Serialize, Deserialize, Debug)] +#[diesel(table_name = issues)] +pub struct NewIssue { + pub id: i32, + pub number: i32, + pub title: String, + pub labels: Option>, + pub open: bool, + pub repository_id: i32, + pub assignee_id: Option, + pub e_tag: String, + pub issue_created_at: DateTime, +} + +#[derive(AsChangeset, Serialize, Deserialize, Debug)] +#[diesel(table_name = issues)] +pub struct UpdateIssue { + pub id: i32, + pub number: i32, + pub title: String, + pub labels: Option>, + pub open: bool, + pub repository_id: Option, + pub assignee_id: Option, + pub e_tag: String, + pub issue_created_at: DateTime, +} + +#[derive(Deserialize, Debug)] +pub struct QueryParams { + pub labels: Option, + pub repository_id: Option, + pub assignee_id: Option, + pub open: Option, +} diff --git a/src/api/issues/routes.rs b/src/api/issues/routes.rs new file mode 100644 index 0000000..790cc79 --- /dev/null +++ b/src/api/issues/routes.rs @@ -0,0 +1,63 @@ +use std::convert::Infallible; + +use warp::filters::BoxedFilter; +use warp::{Filter, Reply}; + +use crate::api::repositories::db::DBRepository; +use crate::auth::with_auth; +use crate::types::PaginationParams; + +use super::db::DBIssue; +use super::handlers; +use super::models::QueryParams; + +fn with_db( + db_pool: impl DBIssue + DBRepository, +) -> impl Filter + Clone { + warp::any().map(move || db_pool.clone()) +} + +pub fn routes(db_access: impl DBIssue + DBRepository) -> BoxedFilter<(impl Reply,)> { + let issue = warp::path!("issues"); + let issue_id = warp::path!("issues" / i32); + + let get_issues = issue + .and(warp::get()) + .and(with_db(db_access.clone())) + .and(warp::query::()) + .and(warp::query::()) + .and_then(handlers::all_handler); + + let get_issue = issue_id + .and(warp::get()) + .and(with_db(db_access.clone())) + .and_then(handlers::by_id); + + let create_issue = issue + .and(with_auth()) + .and(warp::post()) + .and(warp::body::json()) + .and(with_db(db_access.clone())) + .and_then(handlers::create_handler); + + let delete_issue = issue_id + .and(with_auth()) + .and(warp::delete()) + .and(with_db(db_access.clone())) + .and_then(handlers::delete_handler); + + let update_issue = issue_id + .and(with_auth()) + .and(warp::patch()) + .and(warp::body::json()) + .and(with_db(db_access.clone())) + .and_then(handlers::update_handler); + + let route = get_issues + .or(get_issue) + .or(create_issue) + .or(delete_issue) + .or(update_issue); + + route.boxed() +} diff --git a/src/api/mod.rs b/src/api/mod.rs index ea0e553..6a90dba 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,5 +1,5 @@ pub mod health; -// pub mod issue; +pub mod issues; // pub mod languages; pub mod projects; pub mod repositories; diff --git a/src/api/repositories/handlers.rs b/src/api/repositories/handlers.rs index ff96fbf..e40fb2e 100644 --- a/src/api/repositories/handlers.rs +++ b/src/api/repositories/handlers.rs @@ -35,6 +35,7 @@ pub async fn create_handler( match db_access.by_slug(&repo.slug)? { Some(r) => Err(warp::reject::custom(RepositoryError::AlreadyExists(r.id))), None => Ok(with_status( + // check if project exists json(&db_access.create(&repo)?), StatusCode::CREATED, )), diff --git a/src/api/repositories/routes.rs b/src/api/repositories/routes.rs index 212ea83..4677b33 100644 --- a/src/api/repositories/routes.rs +++ b/src/api/repositories/routes.rs @@ -44,7 +44,7 @@ pub fn routes(db_access: impl DBRepository) -> BoxedFilter<(impl Reply,)> { let update_repository = repository_id .and(with_auth()) - .and(warp::post()) + .and(warp::patch()) .and(warp::body::json()) .and(with_db(db_access.clone())) .and_then(handlers::update_handler); diff --git a/src/api/users/routes.rs b/src/api/users/routes.rs index 926b3ba..3964910 100644 --- a/src/api/users/routes.rs +++ b/src/api/users/routes.rs @@ -18,7 +18,6 @@ fn with_db( pub fn routes(db_access: impl DBUser) -> BoxedFilter<(impl Reply,)> { let user = warp::path!("users"); let user_id = warp::path!("users" / i32); - let user_name = warp::path!("users" / "username" / String); let user_maintainer = warp::path!("users" / i32 / "maintainers"); let get_users = user diff --git a/src/main.rs b/src/main.rs index c39f9be..b26e735 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,9 +6,6 @@ mod api; mod auth; mod db; mod errors; -// mod languages; -// mod issue; -// mod user; pub mod schema; mod utils; diff --git a/src/schema.rs b/src/schema.rs index 53b6f5b..6b9cfb2 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,5 +1,23 @@ // @generated automatically by Diesel CLI. +diesel::table! { + issues (id) { + id -> Int4, + number -> Int4, + #[max_length = 100] + title -> Varchar, + labels -> Nullable>>, + open -> Bool, + assignee_id -> Nullable, + #[max_length = 100] + e_tag -> Varchar, + repository_id -> Int4, + issue_created_at -> Timestamptz, + created_at -> Timestamptz, + updated_at -> Nullable, + } +} + diesel::table! { languages (id) { id -> Int4, @@ -50,10 +68,13 @@ diesel::table! { } } +diesel::joinable!(issues -> repositories (repository_id)); +diesel::joinable!(issues -> users (assignee_id)); diesel::joinable!(repositories -> languages (language_id)); diesel::joinable!(repositories -> projects (project_id)); diesel::allow_tables_to_appear_in_same_query!( + issues, languages, projects, repositories, diff --git a/src/utils.rs b/src/utils.rs index ac331b6..8269d01 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,7 +2,7 @@ use ::warp::Reply; use warp::{filters::BoxedFilter, Filter}; use crate::{ - api::{health, projects, repositories}, + api::{health, issues, projects, repositories}, db::{ self, errors::DBError, @@ -43,10 +43,12 @@ pub fn setup_filters(db: DBAccess) -> BoxedFilter<(impl Reply,)> { let health_route = health::routes::routes(db.clone()); let projects_route = projects::routes::routes(db.clone()); let repositories_route = repositories::routes::routes(db.clone()); + let issues_route = issues::routes::routes(db.clone()); health_route .or(projects_route) .or(repositories_route) + .or(issues_route) .with(warp::cors().allow_any_origin()) .recover(error_handler) .boxed() From 351c4910c62b5d8f6cde09439af245f24d86f767 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 12 May 2024 20:01:00 -0300 Subject: [PATCH 28/98] Revert "feat/issues" This reverts commit ef8e235bf155293fcde0a009c632c0878f190e76. --- migrations/2024-04-13-204151_init/down.sql | 4 +- migrations/2024-04-13-204151_init/up.sql | 14 --- src/api/issue/db.rs | 76 +++++++++++++++ src/api/{issues => issue}/errors.rs | 26 +++--- src/api/issue/handlers.rs | 60 ++++++++++++ src/api/{issues => issue}/mod.rs | 1 + src/api/issue/models.rs | 44 +++++++++ src/api/issue/routes.rs | 52 +++++++++++ src/api/issue/utils.rs | 27 ++++++ src/api/issues/db.rs | 102 --------------------- src/api/issues/handlers.rs | 74 --------------- src/api/issues/models.rs | 60 ------------ src/api/issues/routes.rs | 63 ------------- src/api/mod.rs | 2 +- src/api/repositories/handlers.rs | 1 - src/api/repositories/routes.rs | 2 +- src/api/users/routes.rs | 1 + src/main.rs | 3 + src/schema.rs | 21 ----- src/utils.rs | 4 +- 20 files changed, 281 insertions(+), 356 deletions(-) create mode 100644 src/api/issue/db.rs rename src/api/{issues => issue}/errors.rs (54%) create mode 100644 src/api/issue/handlers.rs rename src/api/{issues => issue}/mod.rs (83%) create mode 100644 src/api/issue/models.rs create mode 100644 src/api/issue/routes.rs create mode 100644 src/api/issue/utils.rs delete mode 100644 src/api/issues/db.rs delete mode 100644 src/api/issues/handlers.rs delete mode 100644 src/api/issues/models.rs delete mode 100644 src/api/issues/routes.rs diff --git a/migrations/2024-04-13-204151_init/down.sql b/migrations/2024-04-13-204151_init/down.sql index 79b2107..ab870b3 100644 --- a/migrations/2024-04-13-204151_init/down.sql +++ b/migrations/2024-04-13-204151_init/down.sql @@ -1,5 +1,3 @@ -- Drop tables DROP TABLE IF EXISTS projects; -DROP TABLE IF EXISTS repositories; -DROP TABLE IF EXISTS users; -DROP TABLE IF EXISTS issues; \ No newline at end of file +DROP TABLE IF EXISTS repositories; \ No newline at end of file diff --git a/migrations/2024-04-13-204151_init/up.sql b/migrations/2024-04-13-204151_init/up.sql index 9480e8a..0a0064e 100644 --- a/migrations/2024-04-13-204151_init/up.sql +++ b/migrations/2024-04-13-204151_init/up.sql @@ -32,18 +32,4 @@ CREATE TABLE IF NOT EXISTS users ( username VARCHAR(100) UNIQUE NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NULL -); --- issues -CREATE TABLE IF NOT EXISTS issues ( - id SERIAL PRIMARY KEY, - number int NOT NULL, - title VARCHAR(100) NOT NULL, - labels TEXT [], - open boolean DEFAULT true NOT NULL, - assignee_id INT REFERENCES users(id) NULL, - e_tag VARCHAR(100) NOT NULL, - repository_id INT REFERENCES repositories(id) NOT NULL, - issue_created_at TIMESTAMP WITH TIME ZONE NOT NULL, - created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, - updated_at TIMESTAMP WITH TIME ZONE NULL ); \ No newline at end of file diff --git a/src/api/issue/db.rs b/src/api/issue/db.rs new file mode 100644 index 0000000..64bd6ba --- /dev/null +++ b/src/api/issue/db.rs @@ -0,0 +1,76 @@ +use mobc::async_trait; +use mobc_postgres::tokio_postgres::Row; +use warp::reject; + +use crate::db::{ + pool::DBAccess, + utils::{ + execute_query_with_timeout, query_one_timeout, query_opt_timeout, query_with_timeout, + DB_QUERY_TIMEOUT, + }, +}; + +use super::models::{Issue, IssueCreate}; + +const TABLE: &str = "issues"; + +#[async_trait] +pub trait DBIssue: Send + Sync + Clone + 'static { + async fn get_issue(&self, id: i32) -> Result, reject::Rejection>; + async fn get_issue_by_url(&self, url: &str) -> Result, reject::Rejection>; + async fn get_issues(&self) -> Result, reject::Rejection>; + async fn create_issue(&self, issue: IssueCreate) -> Result; + async fn delete_issue(&self, id: i32) -> Result<(), reject::Rejection>; +} + +#[async_trait] +impl DBIssue for DBAccess { + async fn get_issue(&self, id: i32) -> Result, reject::Rejection> { + let query = format!("SELECT * FROM {} WHERE id = $1", TABLE); + match query_opt_timeout(self, query.as_str(), &[&id], DB_QUERY_TIMEOUT).await? { + Some(user) => Ok(Some(row_to_issue(&user))), + None => Ok(None), + } + } + + async fn get_issue_by_url(&self, url: &str) -> Result, reject::Rejection> { + let query = format!("SELECT * FROM {} WHERE url = $1", TABLE); + match query_opt_timeout(self, query.as_str(), &[&url], DB_QUERY_TIMEOUT).await? { + Some(user) => Ok(Some(row_to_issue(&user))), + None => Ok(None), + } + } + + async fn get_issues(&self) -> Result, reject::Rejection> { + let query = format!("SELECT * FROM {} ORDER BY created_at DESC", TABLE); + let rows = query_with_timeout(self, query.as_str(), &[], DB_QUERY_TIMEOUT).await?; + Ok(rows.iter().map(row_to_issue).collect()) + } + + async fn create_issue(&self, issue: IssueCreate) -> Result { + let query = format!( + "INSERT INTO {} (issue_number, repository_id, url) VALUES ($1, $2, $3) RETURNING *", + TABLE + ); + let row = query_one_timeout( + self, + &query, + &[&issue.id, &issue.repository_id, &issue.url], + DB_QUERY_TIMEOUT, + ) + .await?; + Ok(row_to_issue(&row)) + } + + async fn delete_issue(&self, id: i32) -> Result<(), reject::Rejection> { + let query = format!("DELETE FROM {} WHERE id = $1", TABLE); + execute_query_with_timeout(self, &query, &[&id], DB_QUERY_TIMEOUT).await + } +} + +fn row_to_issue(row: &Row) -> Issue { + let id: i32 = row.get(0); + let repository_id: i32 = row.get(1); + // let url: &str = row.get(2); //TODO: define if we need it in the response + Issue { id, repository_id } +} diff --git a/src/api/issues/errors.rs b/src/api/issue/errors.rs similarity index 54% rename from src/api/issues/errors.rs rename to src/api/issue/errors.rs index ca8a140..92591f8 100644 --- a/src/api/issues/errors.rs +++ b/src/api/issue/errors.rs @@ -8,26 +8,26 @@ use warp::{ reply::{Reply, Response}, }; -use crate::errors::ErrorResponse; +use crate::error_handler::ErrorResponse; #[derive(Clone, Error, Debug, Deserialize, PartialEq)] pub enum IssueError { - AlreadyExists(i32), - NotFound(i32), - InvalidPayload, + IssueExists(i32), + IssueNotFound(i32), + IssueInvalidURL, } impl fmt::Display for IssueError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - IssueError::AlreadyExists(id) => { - write!(f, "Issue #{id} already exists") + IssueError::IssueExists(id) => { + write!(f, "Issue #{} already exists", id) } - IssueError::NotFound(id) => { - write!(f, "Issue #{id} not found") + IssueError::IssueNotFound(id) => { + write!(f, "Issue #{} not found", id) } - IssueError::InvalidPayload => { - write!(f, "Invalid payload") + IssueError::IssueInvalidURL => { + write!(f, "Issue url is invalid") } } } @@ -38,9 +38,9 @@ impl Reject for IssueError {} impl Reply for IssueError { fn into_response(self) -> Response { let code = match self { - IssueError::AlreadyExists(_) => StatusCode::BAD_REQUEST, - IssueError::NotFound(_) => StatusCode::NOT_FOUND, - IssueError::InvalidPayload => StatusCode::UNPROCESSABLE_ENTITY, + IssueError::IssueExists(_) => StatusCode::BAD_REQUEST, + IssueError::IssueNotFound(_) => StatusCode::NOT_FOUND, + IssueError::IssueInvalidURL => StatusCode::BAD_REQUEST, }; let message = self.to_string(); diff --git a/src/api/issue/handlers.rs b/src/api/issue/handlers.rs new file mode 100644 index 0000000..641736f --- /dev/null +++ b/src/api/issue/handlers.rs @@ -0,0 +1,60 @@ +use warp::{ + http::StatusCode, + reject::Rejection, + reply::{json, Reply}, +}; + +use crate::{organization::db::DBOrganization, repository::db::DBRepository}; + +use super::{ + db::DBIssue, + errors::IssueError, + models::{IssueCreateRequest, IssueResponse}, + utils::parse_github_issue_url, +}; + +pub async fn create_issue_handler( + body: IssueCreateRequest, + db_access: impl DBIssue + DBOrganization + DBRepository, +) -> Result { + match db_access.get_issue_by_url(&body.url).await? { + Some(u) => Err(warp::reject::custom(IssueError::IssueExists(u.id)))?, + None => { + _ = parse_github_issue_url(&body.url)?; + // TODO: get or create both + // db_access.create_organization(info.organization); + // db_access.create_repository(info.repository); + // Ok(json(&IssueResponse::of( + // db_access.create_issue(body).await?, + // ))) + Ok(StatusCode::OK) //TODO: added to avoid errors in IDE + } + } +} + +pub async fn get_issue_handler(id: i32, db_access: impl DBIssue) -> Result { + match db_access.get_issue(id).await? { + None => Err(warp::reject::custom(IssueError::IssueNotFound(id)))?, + Some(issue) => Ok(json(&IssueResponse::of(issue))), + } +} + +pub async fn get_issues_handler(db_access: impl DBIssue) -> Result { + let issues = db_access.get_issues().await?; + Ok(json::>( + &issues.into_iter().map(IssueResponse::of).collect(), + )) +} + +pub async fn delete_issue_handler( + id: i32, + db_access: impl DBIssue, +) -> Result { + match db_access.get_issue(id).await? { + Some(_) => { + let _ = &db_access.delete_issue(id).await?; + Ok(StatusCode::OK) + } + None => Err(warp::reject::custom(IssueError::IssueNotFound(id)))?, + } +} diff --git a/src/api/issues/mod.rs b/src/api/issue/mod.rs similarity index 83% rename from src/api/issues/mod.rs rename to src/api/issue/mod.rs index 93246e5..b6041a3 100644 --- a/src/api/issues/mod.rs +++ b/src/api/issue/mod.rs @@ -3,3 +3,4 @@ pub mod errors; pub mod handlers; pub mod models; pub mod routes; +pub mod utils; diff --git a/src/api/issue/models.rs b/src/api/issue/models.rs new file mode 100644 index 0000000..b93a455 --- /dev/null +++ b/src/api/issue/models.rs @@ -0,0 +1,44 @@ +use serde_derive::{Deserialize, Serialize}; + +#[derive(Deserialize)] +pub struct Issue { + pub id: i32, + pub repository_id: i32, +} + +#[derive(Serialize, Deserialize)] +pub struct IssueCreateRequest { + pub url: String, +} +#[derive(Serialize, Deserialize)] +pub struct IssueGetRequest { + pub id: i32, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct IssueResponse { + pub id: i32, + pub repository_id: i32, + // TODO: add tip +} + +impl IssueResponse { + pub fn of(issue: Issue) -> IssueResponse { + IssueResponse { + id: issue.id, + repository_id: issue.repository_id, + } + } +} + +pub struct IssueInfo { + pub organization: String, + pub repository: String, + pub issue_id: u32, +} + +pub struct IssueCreate { + pub repository_id: u32, + pub id: u32, + pub url: String, +} diff --git a/src/api/issue/routes.rs b/src/api/issue/routes.rs new file mode 100644 index 0000000..14d9913 --- /dev/null +++ b/src/api/issue/routes.rs @@ -0,0 +1,52 @@ +use std::convert::Infallible; + +use warp::filters::BoxedFilter; +use warp::{Filter, Reply}; + +use crate::auth::with_auth; +use crate::organization::db::DBOrganization; +use crate::repository::db::DBRepository; + +use super::db::DBIssue; +use super::handlers; + +fn with_db( + db_pool: impl DBIssue + DBOrganization + DBRepository, +) -> impl Filter + Clone +{ + warp::any().map(move || db_pool.clone()) +} + +pub fn routes( + db_access: impl DBIssue + DBOrganization + DBRepository, +) -> BoxedFilter<(impl Reply,)> { + let issue = warp::path!("issues"); + let issue_id = warp::path!("issues" / i32); + + let get_issues = issue + .and(warp::get()) + .and(with_db(db_access.clone())) + .and_then(handlers::get_issues_handler); + + let get_issue = issue_id + .and(warp::get()) + .and(with_db(db_access.clone())) + .and_then(handlers::get_issue_handler); + + let create_issue = issue + .and(with_auth()) + .and(warp::post()) + .and(warp::body::json()) + .and(with_db(db_access.clone())) + .and_then(handlers::create_issue_handler); + + let delete_issue = issue_id + .and(with_auth()) + .and(warp::delete()) + .and(with_db(db_access.clone())) + .and_then(handlers::delete_issue_handler); + + let route = get_issues.or(get_issue).or(create_issue).or(delete_issue); + + route.boxed() +} diff --git a/src/api/issue/utils.rs b/src/api/issue/utils.rs new file mode 100644 index 0000000..fc00ab6 --- /dev/null +++ b/src/api/issue/utils.rs @@ -0,0 +1,27 @@ +use super::{errors::IssueError, models::IssueInfo}; +use url::Url; + +pub fn parse_github_issue_url(url_str: &str) -> Result { + let url = Url::parse(url_str).map_err(|_| IssueError::IssueInvalidURL)?; + let path_segments: Vec<&str> = url + .path_segments() + .ok_or(IssueError::IssueInvalidURL)? + .collect(); + if path_segments.len() >= 4 && path_segments[0] == "github.com" && path_segments[2] == "issues" + { + // Extract organization, repository, and issue id + let organization = path_segments[1].to_owned(); + let repository = path_segments[2].to_owned(); + let issue_id = path_segments[3] + .parse::() + .map_err(|_| IssueError::IssueInvalidURL)?; + + Ok(IssueInfo { + organization, + repository, + issue_id, + }) + } else { + Err(IssueError::IssueInvalidURL) + } +} diff --git a/src/api/issues/db.rs b/src/api/issues/db.rs deleted file mode 100644 index c2f8bfc..0000000 --- a/src/api/issues/db.rs +++ /dev/null @@ -1,102 +0,0 @@ -use diesel::dsl::now; -use diesel::prelude::*; - -use super::models::{Issue, NewIssue, QueryParams, UpdateIssue}; -use crate::schema::issues::dsl as issues_dsl; - -use crate::db::{ - errors::DBError, - pool::{DBAccess, DBAccessor}, -}; -use crate::types::PaginationParams; -use crate::utils; -pub trait DBIssue: Send + Sync + Clone + 'static { - fn all(&self, params: QueryParams, pagination: PaginationParams) - -> Result, DBError>; - fn by_id(&self, id: i32) -> Result, DBError>; - fn by_number(&self, repository_id: i32, number: i32) -> Result, DBError>; - fn create(&self, issue: &NewIssue) -> Result; - fn update(&self, id: i32, issue: &UpdateIssue) -> Result; - fn delete(&self, id: i32) -> Result<(), DBError>; -} - -impl DBIssue for DBAccess { - fn all( - &self, - params: QueryParams, - pagination: PaginationParams, - ) -> Result, DBError> { - let conn = &mut self.get_db_conn(); - let mut query = issues_dsl::issues.into_boxed(); - - if let Some(raw_labels) = params.labels { - let labels: Vec = utils::parse_comma_values(&raw_labels); - query = query.filter(issues_dsl::labels.overlaps_with(labels)); - } - - if let Some(open) = params.open { - query = query.filter(issues_dsl::open.eq(open)); - } - - if let Some(assignee_id) = params.assignee_id { - query = query.filter(issues_dsl::assignee_id.eq(assignee_id)); - } - - if let Some(repository_id) = params.repository_id { - query = query.filter(issues_dsl::repository_id.eq(repository_id)); - } - - query = query.offset(pagination.offset).limit(pagination.limit); - - let result = query.load::(conn)?; - Ok(result) - } - fn by_id(&self, id: i32) -> Result, DBError> { - let conn = &mut self.get_db_conn(); - let result = issues_dsl::issues - .find(id) - .first::(conn) - .optional() - .map_err(DBError::from)?; - Ok(result) - } - fn by_number(&self, repository_id: i32, number: i32) -> Result, DBError> { - let conn = &mut self.get_db_conn(); - let result = issues_dsl::issues - .filter(issues_dsl::repository_id.eq(repository_id)) - .filter(issues_dsl::number.eq(number)) - .first::(conn) - .optional() - .map_err(DBError::from)?; - Ok(result) - } - fn create(&self, form: &NewIssue) -> Result { - let conn = &mut self.get_db_conn(); - let project = diesel::insert_into(issues_dsl::issues) - .values(form) - .get_result(conn) - .map_err(DBError::from)?; - - Ok(project) - } - - fn update(&self, id: i32, issue: &UpdateIssue) -> Result { - let conn = &mut self.get_db_conn(); - - let project = diesel::update(issues_dsl::issues.filter(issues_dsl::id.eq(id))) - .set((issue, issues_dsl::updated_at.eq(now))) - .get_result::(conn) - .map_err(DBError::from)?; - - Ok(project) - } - - fn delete(&self, id: i32) -> Result<(), DBError> { - let conn = &mut self.get_db_conn(); - diesel::delete(issues_dsl::issues.filter(issues_dsl::id.eq(id))) - .execute(conn) - .map_err(DBError::from)?; - - Ok(()) - } -} diff --git a/src/api/issues/handlers.rs b/src/api/issues/handlers.rs deleted file mode 100644 index afc6a55..0000000 --- a/src/api/issues/handlers.rs +++ /dev/null @@ -1,74 +0,0 @@ -use warp::{ - http::StatusCode, - reject::Rejection, - reply::{json, with_status, Reply}, -}; - -use crate::{api::repositories::db::DBRepository, types::PaginationParams}; - -use super::{ - db::DBIssue, - errors::IssueError, - models::{NewIssue, QueryParams, UpdateIssue}, -}; - -pub async fn by_id(id: i32, db_access: impl DBIssue) -> Result { - match db_access.by_id(id)? { - None => Err(warp::reject::custom(IssueError::NotFound(id)))?, - Some(repository) => Ok(json(&repository)), - } -} - -pub async fn all_handler( - db_access: impl DBIssue, - params: QueryParams, - pagination: PaginationParams, -) -> Result { - let issues = db_access.all(params, pagination)?; - Ok(json::>(&issues)) -} - -pub async fn create_handler( - issue: NewIssue, - db_access: impl DBIssue + DBRepository, -) -> Result { - match DBRepository::by_id(&db_access, issue.repository_id) { - Ok(_) => match db_access.by_number(issue.repository_id, issue.number)? { - Some(r) => Err(warp::reject::custom(IssueError::AlreadyExists(r.id))), - None => Ok(with_status( - // check if repository exists - json(&DBIssue::create(&db_access, &issue)?), - StatusCode::CREATED, - )), - }, - Err(_) => Err(warp::reject::custom(IssueError::InvalidPayload)), - } -} -pub async fn update_handler( - id: i32, - issue: UpdateIssue, - db_access: impl DBIssue + DBRepository, -) -> Result { - if let Some(repo_id) = issue.repository_id { - if DBRepository::by_id(&db_access, repo_id).is_err() { - return Err(warp::reject::custom(IssueError::InvalidPayload)); - } - } - - match DBIssue::by_id(&db_access, id)? { - Some(p) => Ok(with_status( - json(&DBIssue::update(&db_access, p.id, &issue)?), - StatusCode::OK, - )), - None => Err(warp::reject::custom(IssueError::NotFound(id))), - } -} -pub async fn delete_handler(id: i32, db_access: impl DBIssue) -> Result { - match DBIssue::by_id(&db_access, id)? { - Some(_) => { - let _ = &db_access.delete(id)?; - Ok(StatusCode::OK) - } - None => Err(warp::reject::custom(IssueError::NotFound(id)))?, - } -} diff --git a/src/api/issues/models.rs b/src/api/issues/models.rs deleted file mode 100644 index 565ff84..0000000 --- a/src/api/issues/models.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::schema::issues; -use chrono::{DateTime, Utc}; -use diesel::prelude::*; - -use serde_derive::{Deserialize, Serialize}; - -#[derive( - AsChangeset, Queryable, Identifiable, Selectable, Debug, PartialEq, Serialize, Deserialize, -)] -#[diesel(table_name = issues)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct Issue { - pub id: i32, - pub number: i32, - // pub title: String, - // pub labels: Option>>, - // pub open: bool, - // pub repository_id: i32, - // pub assignee_id: Option, - // pub e_tag: String, - // pub issue_created_at: DateTime, - // pub created_at: DateTime, - // pub updated_at: Option>, -} - -#[derive(Insertable, Serialize, Deserialize, Debug)] -#[diesel(table_name = issues)] -pub struct NewIssue { - pub id: i32, - pub number: i32, - pub title: String, - pub labels: Option>, - pub open: bool, - pub repository_id: i32, - pub assignee_id: Option, - pub e_tag: String, - pub issue_created_at: DateTime, -} - -#[derive(AsChangeset, Serialize, Deserialize, Debug)] -#[diesel(table_name = issues)] -pub struct UpdateIssue { - pub id: i32, - pub number: i32, - pub title: String, - pub labels: Option>, - pub open: bool, - pub repository_id: Option, - pub assignee_id: Option, - pub e_tag: String, - pub issue_created_at: DateTime, -} - -#[derive(Deserialize, Debug)] -pub struct QueryParams { - pub labels: Option, - pub repository_id: Option, - pub assignee_id: Option, - pub open: Option, -} diff --git a/src/api/issues/routes.rs b/src/api/issues/routes.rs deleted file mode 100644 index 790cc79..0000000 --- a/src/api/issues/routes.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::convert::Infallible; - -use warp::filters::BoxedFilter; -use warp::{Filter, Reply}; - -use crate::api::repositories::db::DBRepository; -use crate::auth::with_auth; -use crate::types::PaginationParams; - -use super::db::DBIssue; -use super::handlers; -use super::models::QueryParams; - -fn with_db( - db_pool: impl DBIssue + DBRepository, -) -> impl Filter + Clone { - warp::any().map(move || db_pool.clone()) -} - -pub fn routes(db_access: impl DBIssue + DBRepository) -> BoxedFilter<(impl Reply,)> { - let issue = warp::path!("issues"); - let issue_id = warp::path!("issues" / i32); - - let get_issues = issue - .and(warp::get()) - .and(with_db(db_access.clone())) - .and(warp::query::()) - .and(warp::query::()) - .and_then(handlers::all_handler); - - let get_issue = issue_id - .and(warp::get()) - .and(with_db(db_access.clone())) - .and_then(handlers::by_id); - - let create_issue = issue - .and(with_auth()) - .and(warp::post()) - .and(warp::body::json()) - .and(with_db(db_access.clone())) - .and_then(handlers::create_handler); - - let delete_issue = issue_id - .and(with_auth()) - .and(warp::delete()) - .and(with_db(db_access.clone())) - .and_then(handlers::delete_handler); - - let update_issue = issue_id - .and(with_auth()) - .and(warp::patch()) - .and(warp::body::json()) - .and(with_db(db_access.clone())) - .and_then(handlers::update_handler); - - let route = get_issues - .or(get_issue) - .or(create_issue) - .or(delete_issue) - .or(update_issue); - - route.boxed() -} diff --git a/src/api/mod.rs b/src/api/mod.rs index 6a90dba..ea0e553 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,5 +1,5 @@ pub mod health; -pub mod issues; +// pub mod issue; // pub mod languages; pub mod projects; pub mod repositories; diff --git a/src/api/repositories/handlers.rs b/src/api/repositories/handlers.rs index e40fb2e..ff96fbf 100644 --- a/src/api/repositories/handlers.rs +++ b/src/api/repositories/handlers.rs @@ -35,7 +35,6 @@ pub async fn create_handler( match db_access.by_slug(&repo.slug)? { Some(r) => Err(warp::reject::custom(RepositoryError::AlreadyExists(r.id))), None => Ok(with_status( - // check if project exists json(&db_access.create(&repo)?), StatusCode::CREATED, )), diff --git a/src/api/repositories/routes.rs b/src/api/repositories/routes.rs index 4677b33..212ea83 100644 --- a/src/api/repositories/routes.rs +++ b/src/api/repositories/routes.rs @@ -44,7 +44,7 @@ pub fn routes(db_access: impl DBRepository) -> BoxedFilter<(impl Reply,)> { let update_repository = repository_id .and(with_auth()) - .and(warp::patch()) + .and(warp::post()) .and(warp::body::json()) .and(with_db(db_access.clone())) .and_then(handlers::update_handler); diff --git a/src/api/users/routes.rs b/src/api/users/routes.rs index 3964910..926b3ba 100644 --- a/src/api/users/routes.rs +++ b/src/api/users/routes.rs @@ -18,6 +18,7 @@ fn with_db( pub fn routes(db_access: impl DBUser) -> BoxedFilter<(impl Reply,)> { let user = warp::path!("users"); let user_id = warp::path!("users" / i32); + let user_name = warp::path!("users" / "username" / String); let user_maintainer = warp::path!("users" / i32 / "maintainers"); let get_users = user diff --git a/src/main.rs b/src/main.rs index b26e735..c39f9be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,9 @@ mod api; mod auth; mod db; mod errors; +// mod languages; +// mod issue; +// mod user; pub mod schema; mod utils; diff --git a/src/schema.rs b/src/schema.rs index 6b9cfb2..53b6f5b 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,23 +1,5 @@ // @generated automatically by Diesel CLI. -diesel::table! { - issues (id) { - id -> Int4, - number -> Int4, - #[max_length = 100] - title -> Varchar, - labels -> Nullable>>, - open -> Bool, - assignee_id -> Nullable, - #[max_length = 100] - e_tag -> Varchar, - repository_id -> Int4, - issue_created_at -> Timestamptz, - created_at -> Timestamptz, - updated_at -> Nullable, - } -} - diesel::table! { languages (id) { id -> Int4, @@ -68,13 +50,10 @@ diesel::table! { } } -diesel::joinable!(issues -> repositories (repository_id)); -diesel::joinable!(issues -> users (assignee_id)); diesel::joinable!(repositories -> languages (language_id)); diesel::joinable!(repositories -> projects (project_id)); diesel::allow_tables_to_appear_in_same_query!( - issues, languages, projects, repositories, diff --git a/src/utils.rs b/src/utils.rs index 8269d01..ac331b6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,7 +2,7 @@ use ::warp::Reply; use warp::{filters::BoxedFilter, Filter}; use crate::{ - api::{health, issues, projects, repositories}, + api::{health, projects, repositories}, db::{ self, errors::DBError, @@ -43,12 +43,10 @@ pub fn setup_filters(db: DBAccess) -> BoxedFilter<(impl Reply,)> { let health_route = health::routes::routes(db.clone()); let projects_route = projects::routes::routes(db.clone()); let repositories_route = repositories::routes::routes(db.clone()); - let issues_route = issues::routes::routes(db.clone()); health_route .or(projects_route) .or(repositories_route) - .or(issues_route) .with(warp::cors().allow_any_origin()) .recover(error_handler) .boxed() From 501a526c6915bfe1799ecb5de2bdc8f34fb10659 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 12 May 2024 20:10:07 -0300 Subject: [PATCH 29/98] Revert "Revert "feat/issues"" This reverts commit 351c4910c62b5d8f6cde09439af245f24d86f767. --- migrations/2024-04-13-204151_init/down.sql | 4 +- migrations/2024-04-13-204151_init/up.sql | 14 +++ src/api/issue/db.rs | 76 --------------- src/api/issue/handlers.rs | 60 ------------ src/api/issue/models.rs | 44 --------- src/api/issue/routes.rs | 52 ----------- src/api/issue/utils.rs | 27 ------ src/api/issues/db.rs | 102 +++++++++++++++++++++ src/api/{issue => issues}/errors.rs | 26 +++--- src/api/issues/handlers.rs | 74 +++++++++++++++ src/api/{issue => issues}/mod.rs | 1 - src/api/issues/models.rs | 60 ++++++++++++ src/api/issues/routes.rs | 63 +++++++++++++ src/api/mod.rs | 2 +- src/api/repositories/handlers.rs | 1 + src/api/repositories/routes.rs | 2 +- src/api/users/routes.rs | 1 - src/main.rs | 3 - src/schema.rs | 21 +++++ src/utils.rs | 4 +- 20 files changed, 356 insertions(+), 281 deletions(-) delete mode 100644 src/api/issue/db.rs delete mode 100644 src/api/issue/handlers.rs delete mode 100644 src/api/issue/models.rs delete mode 100644 src/api/issue/routes.rs delete mode 100644 src/api/issue/utils.rs create mode 100644 src/api/issues/db.rs rename src/api/{issue => issues}/errors.rs (54%) create mode 100644 src/api/issues/handlers.rs rename src/api/{issue => issues}/mod.rs (83%) create mode 100644 src/api/issues/models.rs create mode 100644 src/api/issues/routes.rs diff --git a/migrations/2024-04-13-204151_init/down.sql b/migrations/2024-04-13-204151_init/down.sql index ab870b3..79b2107 100644 --- a/migrations/2024-04-13-204151_init/down.sql +++ b/migrations/2024-04-13-204151_init/down.sql @@ -1,3 +1,5 @@ -- Drop tables DROP TABLE IF EXISTS projects; -DROP TABLE IF EXISTS repositories; \ No newline at end of file +DROP TABLE IF EXISTS repositories; +DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS issues; \ No newline at end of file diff --git a/migrations/2024-04-13-204151_init/up.sql b/migrations/2024-04-13-204151_init/up.sql index 0a0064e..9480e8a 100644 --- a/migrations/2024-04-13-204151_init/up.sql +++ b/migrations/2024-04-13-204151_init/up.sql @@ -32,4 +32,18 @@ CREATE TABLE IF NOT EXISTS users ( username VARCHAR(100) UNIQUE NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NULL +); +-- issues +CREATE TABLE IF NOT EXISTS issues ( + id SERIAL PRIMARY KEY, + number int NOT NULL, + title VARCHAR(100) NOT NULL, + labels TEXT [], + open boolean DEFAULT true NOT NULL, + assignee_id INT REFERENCES users(id) NULL, + e_tag VARCHAR(100) NOT NULL, + repository_id INT REFERENCES repositories(id) NOT NULL, + issue_created_at TIMESTAMP WITH TIME ZONE NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE NULL ); \ No newline at end of file diff --git a/src/api/issue/db.rs b/src/api/issue/db.rs deleted file mode 100644 index 64bd6ba..0000000 --- a/src/api/issue/db.rs +++ /dev/null @@ -1,76 +0,0 @@ -use mobc::async_trait; -use mobc_postgres::tokio_postgres::Row; -use warp::reject; - -use crate::db::{ - pool::DBAccess, - utils::{ - execute_query_with_timeout, query_one_timeout, query_opt_timeout, query_with_timeout, - DB_QUERY_TIMEOUT, - }, -}; - -use super::models::{Issue, IssueCreate}; - -const TABLE: &str = "issues"; - -#[async_trait] -pub trait DBIssue: Send + Sync + Clone + 'static { - async fn get_issue(&self, id: i32) -> Result, reject::Rejection>; - async fn get_issue_by_url(&self, url: &str) -> Result, reject::Rejection>; - async fn get_issues(&self) -> Result, reject::Rejection>; - async fn create_issue(&self, issue: IssueCreate) -> Result; - async fn delete_issue(&self, id: i32) -> Result<(), reject::Rejection>; -} - -#[async_trait] -impl DBIssue for DBAccess { - async fn get_issue(&self, id: i32) -> Result, reject::Rejection> { - let query = format!("SELECT * FROM {} WHERE id = $1", TABLE); - match query_opt_timeout(self, query.as_str(), &[&id], DB_QUERY_TIMEOUT).await? { - Some(user) => Ok(Some(row_to_issue(&user))), - None => Ok(None), - } - } - - async fn get_issue_by_url(&self, url: &str) -> Result, reject::Rejection> { - let query = format!("SELECT * FROM {} WHERE url = $1", TABLE); - match query_opt_timeout(self, query.as_str(), &[&url], DB_QUERY_TIMEOUT).await? { - Some(user) => Ok(Some(row_to_issue(&user))), - None => Ok(None), - } - } - - async fn get_issues(&self) -> Result, reject::Rejection> { - let query = format!("SELECT * FROM {} ORDER BY created_at DESC", TABLE); - let rows = query_with_timeout(self, query.as_str(), &[], DB_QUERY_TIMEOUT).await?; - Ok(rows.iter().map(row_to_issue).collect()) - } - - async fn create_issue(&self, issue: IssueCreate) -> Result { - let query = format!( - "INSERT INTO {} (issue_number, repository_id, url) VALUES ($1, $2, $3) RETURNING *", - TABLE - ); - let row = query_one_timeout( - self, - &query, - &[&issue.id, &issue.repository_id, &issue.url], - DB_QUERY_TIMEOUT, - ) - .await?; - Ok(row_to_issue(&row)) - } - - async fn delete_issue(&self, id: i32) -> Result<(), reject::Rejection> { - let query = format!("DELETE FROM {} WHERE id = $1", TABLE); - execute_query_with_timeout(self, &query, &[&id], DB_QUERY_TIMEOUT).await - } -} - -fn row_to_issue(row: &Row) -> Issue { - let id: i32 = row.get(0); - let repository_id: i32 = row.get(1); - // let url: &str = row.get(2); //TODO: define if we need it in the response - Issue { id, repository_id } -} diff --git a/src/api/issue/handlers.rs b/src/api/issue/handlers.rs deleted file mode 100644 index 641736f..0000000 --- a/src/api/issue/handlers.rs +++ /dev/null @@ -1,60 +0,0 @@ -use warp::{ - http::StatusCode, - reject::Rejection, - reply::{json, Reply}, -}; - -use crate::{organization::db::DBOrganization, repository::db::DBRepository}; - -use super::{ - db::DBIssue, - errors::IssueError, - models::{IssueCreateRequest, IssueResponse}, - utils::parse_github_issue_url, -}; - -pub async fn create_issue_handler( - body: IssueCreateRequest, - db_access: impl DBIssue + DBOrganization + DBRepository, -) -> Result { - match db_access.get_issue_by_url(&body.url).await? { - Some(u) => Err(warp::reject::custom(IssueError::IssueExists(u.id)))?, - None => { - _ = parse_github_issue_url(&body.url)?; - // TODO: get or create both - // db_access.create_organization(info.organization); - // db_access.create_repository(info.repository); - // Ok(json(&IssueResponse::of( - // db_access.create_issue(body).await?, - // ))) - Ok(StatusCode::OK) //TODO: added to avoid errors in IDE - } - } -} - -pub async fn get_issue_handler(id: i32, db_access: impl DBIssue) -> Result { - match db_access.get_issue(id).await? { - None => Err(warp::reject::custom(IssueError::IssueNotFound(id)))?, - Some(issue) => Ok(json(&IssueResponse::of(issue))), - } -} - -pub async fn get_issues_handler(db_access: impl DBIssue) -> Result { - let issues = db_access.get_issues().await?; - Ok(json::>( - &issues.into_iter().map(IssueResponse::of).collect(), - )) -} - -pub async fn delete_issue_handler( - id: i32, - db_access: impl DBIssue, -) -> Result { - match db_access.get_issue(id).await? { - Some(_) => { - let _ = &db_access.delete_issue(id).await?; - Ok(StatusCode::OK) - } - None => Err(warp::reject::custom(IssueError::IssueNotFound(id)))?, - } -} diff --git a/src/api/issue/models.rs b/src/api/issue/models.rs deleted file mode 100644 index b93a455..0000000 --- a/src/api/issue/models.rs +++ /dev/null @@ -1,44 +0,0 @@ -use serde_derive::{Deserialize, Serialize}; - -#[derive(Deserialize)] -pub struct Issue { - pub id: i32, - pub repository_id: i32, -} - -#[derive(Serialize, Deserialize)] -pub struct IssueCreateRequest { - pub url: String, -} -#[derive(Serialize, Deserialize)] -pub struct IssueGetRequest { - pub id: i32, -} - -#[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct IssueResponse { - pub id: i32, - pub repository_id: i32, - // TODO: add tip -} - -impl IssueResponse { - pub fn of(issue: Issue) -> IssueResponse { - IssueResponse { - id: issue.id, - repository_id: issue.repository_id, - } - } -} - -pub struct IssueInfo { - pub organization: String, - pub repository: String, - pub issue_id: u32, -} - -pub struct IssueCreate { - pub repository_id: u32, - pub id: u32, - pub url: String, -} diff --git a/src/api/issue/routes.rs b/src/api/issue/routes.rs deleted file mode 100644 index 14d9913..0000000 --- a/src/api/issue/routes.rs +++ /dev/null @@ -1,52 +0,0 @@ -use std::convert::Infallible; - -use warp::filters::BoxedFilter; -use warp::{Filter, Reply}; - -use crate::auth::with_auth; -use crate::organization::db::DBOrganization; -use crate::repository::db::DBRepository; - -use super::db::DBIssue; -use super::handlers; - -fn with_db( - db_pool: impl DBIssue + DBOrganization + DBRepository, -) -> impl Filter + Clone -{ - warp::any().map(move || db_pool.clone()) -} - -pub fn routes( - db_access: impl DBIssue + DBOrganization + DBRepository, -) -> BoxedFilter<(impl Reply,)> { - let issue = warp::path!("issues"); - let issue_id = warp::path!("issues" / i32); - - let get_issues = issue - .and(warp::get()) - .and(with_db(db_access.clone())) - .and_then(handlers::get_issues_handler); - - let get_issue = issue_id - .and(warp::get()) - .and(with_db(db_access.clone())) - .and_then(handlers::get_issue_handler); - - let create_issue = issue - .and(with_auth()) - .and(warp::post()) - .and(warp::body::json()) - .and(with_db(db_access.clone())) - .and_then(handlers::create_issue_handler); - - let delete_issue = issue_id - .and(with_auth()) - .and(warp::delete()) - .and(with_db(db_access.clone())) - .and_then(handlers::delete_issue_handler); - - let route = get_issues.or(get_issue).or(create_issue).or(delete_issue); - - route.boxed() -} diff --git a/src/api/issue/utils.rs b/src/api/issue/utils.rs deleted file mode 100644 index fc00ab6..0000000 --- a/src/api/issue/utils.rs +++ /dev/null @@ -1,27 +0,0 @@ -use super::{errors::IssueError, models::IssueInfo}; -use url::Url; - -pub fn parse_github_issue_url(url_str: &str) -> Result { - let url = Url::parse(url_str).map_err(|_| IssueError::IssueInvalidURL)?; - let path_segments: Vec<&str> = url - .path_segments() - .ok_or(IssueError::IssueInvalidURL)? - .collect(); - if path_segments.len() >= 4 && path_segments[0] == "github.com" && path_segments[2] == "issues" - { - // Extract organization, repository, and issue id - let organization = path_segments[1].to_owned(); - let repository = path_segments[2].to_owned(); - let issue_id = path_segments[3] - .parse::() - .map_err(|_| IssueError::IssueInvalidURL)?; - - Ok(IssueInfo { - organization, - repository, - issue_id, - }) - } else { - Err(IssueError::IssueInvalidURL) - } -} diff --git a/src/api/issues/db.rs b/src/api/issues/db.rs new file mode 100644 index 0000000..c2f8bfc --- /dev/null +++ b/src/api/issues/db.rs @@ -0,0 +1,102 @@ +use diesel::dsl::now; +use diesel::prelude::*; + +use super::models::{Issue, NewIssue, QueryParams, UpdateIssue}; +use crate::schema::issues::dsl as issues_dsl; + +use crate::db::{ + errors::DBError, + pool::{DBAccess, DBAccessor}, +}; +use crate::types::PaginationParams; +use crate::utils; +pub trait DBIssue: Send + Sync + Clone + 'static { + fn all(&self, params: QueryParams, pagination: PaginationParams) + -> Result, DBError>; + fn by_id(&self, id: i32) -> Result, DBError>; + fn by_number(&self, repository_id: i32, number: i32) -> Result, DBError>; + fn create(&self, issue: &NewIssue) -> Result; + fn update(&self, id: i32, issue: &UpdateIssue) -> Result; + fn delete(&self, id: i32) -> Result<(), DBError>; +} + +impl DBIssue for DBAccess { + fn all( + &self, + params: QueryParams, + pagination: PaginationParams, + ) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + let mut query = issues_dsl::issues.into_boxed(); + + if let Some(raw_labels) = params.labels { + let labels: Vec = utils::parse_comma_values(&raw_labels); + query = query.filter(issues_dsl::labels.overlaps_with(labels)); + } + + if let Some(open) = params.open { + query = query.filter(issues_dsl::open.eq(open)); + } + + if let Some(assignee_id) = params.assignee_id { + query = query.filter(issues_dsl::assignee_id.eq(assignee_id)); + } + + if let Some(repository_id) = params.repository_id { + query = query.filter(issues_dsl::repository_id.eq(repository_id)); + } + + query = query.offset(pagination.offset).limit(pagination.limit); + + let result = query.load::(conn)?; + Ok(result) + } + fn by_id(&self, id: i32) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + let result = issues_dsl::issues + .find(id) + .first::(conn) + .optional() + .map_err(DBError::from)?; + Ok(result) + } + fn by_number(&self, repository_id: i32, number: i32) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + let result = issues_dsl::issues + .filter(issues_dsl::repository_id.eq(repository_id)) + .filter(issues_dsl::number.eq(number)) + .first::(conn) + .optional() + .map_err(DBError::from)?; + Ok(result) + } + fn create(&self, form: &NewIssue) -> Result { + let conn = &mut self.get_db_conn(); + let project = diesel::insert_into(issues_dsl::issues) + .values(form) + .get_result(conn) + .map_err(DBError::from)?; + + Ok(project) + } + + fn update(&self, id: i32, issue: &UpdateIssue) -> Result { + let conn = &mut self.get_db_conn(); + + let project = diesel::update(issues_dsl::issues.filter(issues_dsl::id.eq(id))) + .set((issue, issues_dsl::updated_at.eq(now))) + .get_result::(conn) + .map_err(DBError::from)?; + + Ok(project) + } + + fn delete(&self, id: i32) -> Result<(), DBError> { + let conn = &mut self.get_db_conn(); + diesel::delete(issues_dsl::issues.filter(issues_dsl::id.eq(id))) + .execute(conn) + .map_err(DBError::from)?; + + Ok(()) + } +} diff --git a/src/api/issue/errors.rs b/src/api/issues/errors.rs similarity index 54% rename from src/api/issue/errors.rs rename to src/api/issues/errors.rs index 92591f8..ca8a140 100644 --- a/src/api/issue/errors.rs +++ b/src/api/issues/errors.rs @@ -8,26 +8,26 @@ use warp::{ reply::{Reply, Response}, }; -use crate::error_handler::ErrorResponse; +use crate::errors::ErrorResponse; #[derive(Clone, Error, Debug, Deserialize, PartialEq)] pub enum IssueError { - IssueExists(i32), - IssueNotFound(i32), - IssueInvalidURL, + AlreadyExists(i32), + NotFound(i32), + InvalidPayload, } impl fmt::Display for IssueError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - IssueError::IssueExists(id) => { - write!(f, "Issue #{} already exists", id) + IssueError::AlreadyExists(id) => { + write!(f, "Issue #{id} already exists") } - IssueError::IssueNotFound(id) => { - write!(f, "Issue #{} not found", id) + IssueError::NotFound(id) => { + write!(f, "Issue #{id} not found") } - IssueError::IssueInvalidURL => { - write!(f, "Issue url is invalid") + IssueError::InvalidPayload => { + write!(f, "Invalid payload") } } } @@ -38,9 +38,9 @@ impl Reject for IssueError {} impl Reply for IssueError { fn into_response(self) -> Response { let code = match self { - IssueError::IssueExists(_) => StatusCode::BAD_REQUEST, - IssueError::IssueNotFound(_) => StatusCode::NOT_FOUND, - IssueError::IssueInvalidURL => StatusCode::BAD_REQUEST, + IssueError::AlreadyExists(_) => StatusCode::BAD_REQUEST, + IssueError::NotFound(_) => StatusCode::NOT_FOUND, + IssueError::InvalidPayload => StatusCode::UNPROCESSABLE_ENTITY, }; let message = self.to_string(); diff --git a/src/api/issues/handlers.rs b/src/api/issues/handlers.rs new file mode 100644 index 0000000..afc6a55 --- /dev/null +++ b/src/api/issues/handlers.rs @@ -0,0 +1,74 @@ +use warp::{ + http::StatusCode, + reject::Rejection, + reply::{json, with_status, Reply}, +}; + +use crate::{api::repositories::db::DBRepository, types::PaginationParams}; + +use super::{ + db::DBIssue, + errors::IssueError, + models::{NewIssue, QueryParams, UpdateIssue}, +}; + +pub async fn by_id(id: i32, db_access: impl DBIssue) -> Result { + match db_access.by_id(id)? { + None => Err(warp::reject::custom(IssueError::NotFound(id)))?, + Some(repository) => Ok(json(&repository)), + } +} + +pub async fn all_handler( + db_access: impl DBIssue, + params: QueryParams, + pagination: PaginationParams, +) -> Result { + let issues = db_access.all(params, pagination)?; + Ok(json::>(&issues)) +} + +pub async fn create_handler( + issue: NewIssue, + db_access: impl DBIssue + DBRepository, +) -> Result { + match DBRepository::by_id(&db_access, issue.repository_id) { + Ok(_) => match db_access.by_number(issue.repository_id, issue.number)? { + Some(r) => Err(warp::reject::custom(IssueError::AlreadyExists(r.id))), + None => Ok(with_status( + // check if repository exists + json(&DBIssue::create(&db_access, &issue)?), + StatusCode::CREATED, + )), + }, + Err(_) => Err(warp::reject::custom(IssueError::InvalidPayload)), + } +} +pub async fn update_handler( + id: i32, + issue: UpdateIssue, + db_access: impl DBIssue + DBRepository, +) -> Result { + if let Some(repo_id) = issue.repository_id { + if DBRepository::by_id(&db_access, repo_id).is_err() { + return Err(warp::reject::custom(IssueError::InvalidPayload)); + } + } + + match DBIssue::by_id(&db_access, id)? { + Some(p) => Ok(with_status( + json(&DBIssue::update(&db_access, p.id, &issue)?), + StatusCode::OK, + )), + None => Err(warp::reject::custom(IssueError::NotFound(id))), + } +} +pub async fn delete_handler(id: i32, db_access: impl DBIssue) -> Result { + match DBIssue::by_id(&db_access, id)? { + Some(_) => { + let _ = &db_access.delete(id)?; + Ok(StatusCode::OK) + } + None => Err(warp::reject::custom(IssueError::NotFound(id)))?, + } +} diff --git a/src/api/issue/mod.rs b/src/api/issues/mod.rs similarity index 83% rename from src/api/issue/mod.rs rename to src/api/issues/mod.rs index b6041a3..93246e5 100644 --- a/src/api/issue/mod.rs +++ b/src/api/issues/mod.rs @@ -3,4 +3,3 @@ pub mod errors; pub mod handlers; pub mod models; pub mod routes; -pub mod utils; diff --git a/src/api/issues/models.rs b/src/api/issues/models.rs new file mode 100644 index 0000000..565ff84 --- /dev/null +++ b/src/api/issues/models.rs @@ -0,0 +1,60 @@ +use crate::schema::issues; +use chrono::{DateTime, Utc}; +use diesel::prelude::*; + +use serde_derive::{Deserialize, Serialize}; + +#[derive( + AsChangeset, Queryable, Identifiable, Selectable, Debug, PartialEq, Serialize, Deserialize, +)] +#[diesel(table_name = issues)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Issue { + pub id: i32, + pub number: i32, + // pub title: String, + // pub labels: Option>>, + // pub open: bool, + // pub repository_id: i32, + // pub assignee_id: Option, + // pub e_tag: String, + // pub issue_created_at: DateTime, + // pub created_at: DateTime, + // pub updated_at: Option>, +} + +#[derive(Insertable, Serialize, Deserialize, Debug)] +#[diesel(table_name = issues)] +pub struct NewIssue { + pub id: i32, + pub number: i32, + pub title: String, + pub labels: Option>, + pub open: bool, + pub repository_id: i32, + pub assignee_id: Option, + pub e_tag: String, + pub issue_created_at: DateTime, +} + +#[derive(AsChangeset, Serialize, Deserialize, Debug)] +#[diesel(table_name = issues)] +pub struct UpdateIssue { + pub id: i32, + pub number: i32, + pub title: String, + pub labels: Option>, + pub open: bool, + pub repository_id: Option, + pub assignee_id: Option, + pub e_tag: String, + pub issue_created_at: DateTime, +} + +#[derive(Deserialize, Debug)] +pub struct QueryParams { + pub labels: Option, + pub repository_id: Option, + pub assignee_id: Option, + pub open: Option, +} diff --git a/src/api/issues/routes.rs b/src/api/issues/routes.rs new file mode 100644 index 0000000..790cc79 --- /dev/null +++ b/src/api/issues/routes.rs @@ -0,0 +1,63 @@ +use std::convert::Infallible; + +use warp::filters::BoxedFilter; +use warp::{Filter, Reply}; + +use crate::api::repositories::db::DBRepository; +use crate::auth::with_auth; +use crate::types::PaginationParams; + +use super::db::DBIssue; +use super::handlers; +use super::models::QueryParams; + +fn with_db( + db_pool: impl DBIssue + DBRepository, +) -> impl Filter + Clone { + warp::any().map(move || db_pool.clone()) +} + +pub fn routes(db_access: impl DBIssue + DBRepository) -> BoxedFilter<(impl Reply,)> { + let issue = warp::path!("issues"); + let issue_id = warp::path!("issues" / i32); + + let get_issues = issue + .and(warp::get()) + .and(with_db(db_access.clone())) + .and(warp::query::()) + .and(warp::query::()) + .and_then(handlers::all_handler); + + let get_issue = issue_id + .and(warp::get()) + .and(with_db(db_access.clone())) + .and_then(handlers::by_id); + + let create_issue = issue + .and(with_auth()) + .and(warp::post()) + .and(warp::body::json()) + .and(with_db(db_access.clone())) + .and_then(handlers::create_handler); + + let delete_issue = issue_id + .and(with_auth()) + .and(warp::delete()) + .and(with_db(db_access.clone())) + .and_then(handlers::delete_handler); + + let update_issue = issue_id + .and(with_auth()) + .and(warp::patch()) + .and(warp::body::json()) + .and(with_db(db_access.clone())) + .and_then(handlers::update_handler); + + let route = get_issues + .or(get_issue) + .or(create_issue) + .or(delete_issue) + .or(update_issue); + + route.boxed() +} diff --git a/src/api/mod.rs b/src/api/mod.rs index ea0e553..6a90dba 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,5 +1,5 @@ pub mod health; -// pub mod issue; +pub mod issues; // pub mod languages; pub mod projects; pub mod repositories; diff --git a/src/api/repositories/handlers.rs b/src/api/repositories/handlers.rs index ff96fbf..e40fb2e 100644 --- a/src/api/repositories/handlers.rs +++ b/src/api/repositories/handlers.rs @@ -35,6 +35,7 @@ pub async fn create_handler( match db_access.by_slug(&repo.slug)? { Some(r) => Err(warp::reject::custom(RepositoryError::AlreadyExists(r.id))), None => Ok(with_status( + // check if project exists json(&db_access.create(&repo)?), StatusCode::CREATED, )), diff --git a/src/api/repositories/routes.rs b/src/api/repositories/routes.rs index 212ea83..4677b33 100644 --- a/src/api/repositories/routes.rs +++ b/src/api/repositories/routes.rs @@ -44,7 +44,7 @@ pub fn routes(db_access: impl DBRepository) -> BoxedFilter<(impl Reply,)> { let update_repository = repository_id .and(with_auth()) - .and(warp::post()) + .and(warp::patch()) .and(warp::body::json()) .and(with_db(db_access.clone())) .and_then(handlers::update_handler); diff --git a/src/api/users/routes.rs b/src/api/users/routes.rs index 926b3ba..3964910 100644 --- a/src/api/users/routes.rs +++ b/src/api/users/routes.rs @@ -18,7 +18,6 @@ fn with_db( pub fn routes(db_access: impl DBUser) -> BoxedFilter<(impl Reply,)> { let user = warp::path!("users"); let user_id = warp::path!("users" / i32); - let user_name = warp::path!("users" / "username" / String); let user_maintainer = warp::path!("users" / i32 / "maintainers"); let get_users = user diff --git a/src/main.rs b/src/main.rs index c39f9be..b26e735 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,9 +6,6 @@ mod api; mod auth; mod db; mod errors; -// mod languages; -// mod issue; -// mod user; pub mod schema; mod utils; diff --git a/src/schema.rs b/src/schema.rs index 53b6f5b..6b9cfb2 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,5 +1,23 @@ // @generated automatically by Diesel CLI. +diesel::table! { + issues (id) { + id -> Int4, + number -> Int4, + #[max_length = 100] + title -> Varchar, + labels -> Nullable>>, + open -> Bool, + assignee_id -> Nullable, + #[max_length = 100] + e_tag -> Varchar, + repository_id -> Int4, + issue_created_at -> Timestamptz, + created_at -> Timestamptz, + updated_at -> Nullable, + } +} + diesel::table! { languages (id) { id -> Int4, @@ -50,10 +68,13 @@ diesel::table! { } } +diesel::joinable!(issues -> repositories (repository_id)); +diesel::joinable!(issues -> users (assignee_id)); diesel::joinable!(repositories -> languages (language_id)); diesel::joinable!(repositories -> projects (project_id)); diesel::allow_tables_to_appear_in_same_query!( + issues, languages, projects, repositories, diff --git a/src/utils.rs b/src/utils.rs index ac331b6..8269d01 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,7 +2,7 @@ use ::warp::Reply; use warp::{filters::BoxedFilter, Filter}; use crate::{ - api::{health, projects, repositories}, + api::{health, issues, projects, repositories}, db::{ self, errors::DBError, @@ -43,10 +43,12 @@ pub fn setup_filters(db: DBAccess) -> BoxedFilter<(impl Reply,)> { let health_route = health::routes::routes(db.clone()); let projects_route = projects::routes::routes(db.clone()); let repositories_route = repositories::routes::routes(db.clone()); + let issues_route = issues::routes::routes(db.clone()); health_route .or(projects_route) .or(repositories_route) + .or(issues_route) .with(warp::cors().allow_any_origin()) .recover(error_handler) .boxed() From 588eb40c7aa6efec6774d2b6c2626f3a45f7eaa3 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 12 May 2024 20:12:57 -0300 Subject: [PATCH 30/98] chore: comment issues --- src/api/mod.rs | 2 +- src/utils.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 6a90dba..8e1a2b7 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,5 +1,5 @@ pub mod health; -pub mod issues; +// pub mod issues; // pub mod languages; pub mod projects; pub mod repositories; diff --git a/src/utils.rs b/src/utils.rs index 8269d01..c03aa6d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,7 +2,7 @@ use ::warp::Reply; use warp::{filters::BoxedFilter, Filter}; use crate::{ - api::{health, issues, projects, repositories}, + api::{health, projects, repositories}, db::{ self, errors::DBError, @@ -43,12 +43,12 @@ pub fn setup_filters(db: DBAccess) -> BoxedFilter<(impl Reply,)> { let health_route = health::routes::routes(db.clone()); let projects_route = projects::routes::routes(db.clone()); let repositories_route = repositories::routes::routes(db.clone()); - let issues_route = issues::routes::routes(db.clone()); + // let issues_route = issues::routes::routes(db.clone()); health_route .or(projects_route) .or(repositories_route) - .or(issues_route) + // .or(issues_route) .with(warp::cors().allow_any_origin()) .recover(error_handler) .boxed() From c749b24af1c3952db5183fab3f6f7612c71ed9bf Mon Sep 17 00:00:00 2001 From: Leandro Date: Sat, 18 May 2024 18:43:16 -0300 Subject: [PATCH 31/98] fix: issues and error handler --- src/api/issues/errors.rs | 8 +-- src/api/issues/handlers.rs | 32 ++++++----- src/api/issues/models.rs | 18 +++---- src/api/mod.rs | 2 +- src/api/projects/db.rs | 10 ++-- src/api/projects/handlers.rs | 6 +-- src/api/projects/models.rs | 4 +- src/errors.rs | 102 +++++++++++++++++++---------------- src/utils.rs | 8 +-- 9 files changed, 102 insertions(+), 88 deletions(-) diff --git a/src/api/issues/errors.rs b/src/api/issues/errors.rs index ca8a140..235a1d1 100644 --- a/src/api/issues/errors.rs +++ b/src/api/issues/errors.rs @@ -14,7 +14,7 @@ use crate::errors::ErrorResponse; pub enum IssueError { AlreadyExists(i32), NotFound(i32), - InvalidPayload, + RepositoryNotFound(i32), } impl fmt::Display for IssueError { @@ -26,8 +26,8 @@ impl fmt::Display for IssueError { IssueError::NotFound(id) => { write!(f, "Issue #{id} not found") } - IssueError::InvalidPayload => { - write!(f, "Invalid payload") + IssueError::RepositoryNotFound(id) => { + write!(f, "Repository #{id} not found") } } } @@ -40,7 +40,7 @@ impl Reply for IssueError { let code = match self { IssueError::AlreadyExists(_) => StatusCode::BAD_REQUEST, IssueError::NotFound(_) => StatusCode::NOT_FOUND, - IssueError::InvalidPayload => StatusCode::UNPROCESSABLE_ENTITY, + IssueError::RepositoryNotFound(_) => StatusCode::UNPROCESSABLE_ENTITY, }; let message = self.to_string(); diff --git a/src/api/issues/handlers.rs b/src/api/issues/handlers.rs index afc6a55..ca5574b 100644 --- a/src/api/issues/handlers.rs +++ b/src/api/issues/handlers.rs @@ -32,16 +32,17 @@ pub async fn create_handler( issue: NewIssue, db_access: impl DBIssue + DBRepository, ) -> Result { - match DBRepository::by_id(&db_access, issue.repository_id) { - Ok(_) => match db_access.by_number(issue.repository_id, issue.number)? { + match DBRepository::by_id(&db_access, issue.repository_id)? { + Some(_) => match db_access.by_number(issue.repository_id, issue.number)? { Some(r) => Err(warp::reject::custom(IssueError::AlreadyExists(r.id))), None => Ok(with_status( - // check if repository exists json(&DBIssue::create(&db_access, &issue)?), StatusCode::CREATED, )), }, - Err(_) => Err(warp::reject::custom(IssueError::InvalidPayload)), + None => Err(warp::reject::custom(IssueError::RepositoryNotFound( + issue.repository_id, + ))), } } pub async fn update_handler( @@ -49,17 +50,20 @@ pub async fn update_handler( issue: UpdateIssue, db_access: impl DBIssue + DBRepository, ) -> Result { - if let Some(repo_id) = issue.repository_id { - if DBRepository::by_id(&db_access, repo_id).is_err() { - return Err(warp::reject::custom(IssueError::InvalidPayload)); - } - } - match DBIssue::by_id(&db_access, id)? { - Some(p) => Ok(with_status( - json(&DBIssue::update(&db_access, p.id, &issue)?), - StatusCode::OK, - )), + Some(p) => { + if let Some(repo_id) = issue.repository_id { + if DBRepository::by_id(&db_access, repo_id)?.is_none() { + return Err(warp::reject::custom(IssueError::RepositoryNotFound( + repo_id, + ))); + } + } + Ok(with_status( + json(&DBIssue::update(&db_access, p.id, &issue)?), + StatusCode::OK, + )) + } None => Err(warp::reject::custom(IssueError::NotFound(id))), } } diff --git a/src/api/issues/models.rs b/src/api/issues/models.rs index 565ff84..47243c2 100644 --- a/src/api/issues/models.rs +++ b/src/api/issues/models.rs @@ -12,15 +12,15 @@ use serde_derive::{Deserialize, Serialize}; pub struct Issue { pub id: i32, pub number: i32, - // pub title: String, - // pub labels: Option>>, - // pub open: bool, - // pub repository_id: i32, - // pub assignee_id: Option, - // pub e_tag: String, - // pub issue_created_at: DateTime, - // pub created_at: DateTime, - // pub updated_at: Option>, + pub title: String, + pub labels: Option>>, + pub open: bool, + pub assignee_id: Option, + pub e_tag: String, + pub repository_id: i32, + pub issue_created_at: DateTime, + pub created_at: DateTime, + pub updated_at: Option>, } #[derive(Insertable, Serialize, Deserialize, Debug)] diff --git a/src/api/mod.rs b/src/api/mod.rs index 8e1a2b7..6a90dba 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,5 +1,5 @@ pub mod health; -// pub mod issues; +pub mod issues; // pub mod languages; pub mod projects; pub mod repositories; diff --git a/src/api/projects/db.rs b/src/api/projects/db.rs index 6e09950..e943bf1 100644 --- a/src/api/projects/db.rs +++ b/src/api/projects/db.rs @@ -1,7 +1,7 @@ use diesel::dsl::now; use diesel::prelude::*; -use super::models::{NewForm, Project, QueryParams, UpdateForm}; +use super::models::{NewProject, Project, QueryParams, UpdateProject}; use crate::schema::projects::dsl as projects_dsl; use crate::db::{ @@ -19,8 +19,8 @@ pub trait DBProject: Send + Sync + Clone + 'static { ) -> Result, DBError>; fn by_id(&self, id: i32) -> Result, DBError>; fn by_slug(&self, slug: &str) -> Result, DBError>; - fn create(&self, form: &NewForm) -> Result; - fn update(&self, id: i32, form: &UpdateForm) -> Result; + fn create(&self, form: &NewProject) -> Result; + fn update(&self, id: i32, form: &UpdateProject) -> Result; fn delete(&self, id: i32) -> Result<(), DBError>; } @@ -82,7 +82,7 @@ impl DBProject for DBAccess { Ok(result) } - fn create(&self, form: &NewForm) -> Result { + fn create(&self, form: &NewProject) -> Result { let conn = &mut self.get_db_conn(); let project = diesel::insert_into(projects_dsl::projects) @@ -93,7 +93,7 @@ impl DBProject for DBAccess { Ok(project) } - fn update(&self, id: i32, form: &UpdateForm) -> Result { + fn update(&self, id: i32, form: &UpdateProject) -> Result { let conn = &mut self.get_db_conn(); let project = diesel::update(projects_dsl::projects.filter(projects_dsl::id.eq(id))) diff --git a/src/api/projects/handlers.rs b/src/api/projects/handlers.rs index b2aa7ba..77938e5 100644 --- a/src/api/projects/handlers.rs +++ b/src/api/projects/handlers.rs @@ -3,7 +3,7 @@ use crate::types::PaginationParams; use super::{ db::DBProject, errors::ProjectError, - models::{NewForm, QueryParams, UpdateForm}, + models::{NewProject, QueryParams, UpdateProject}, }; use warp::{ http::StatusCode, @@ -21,7 +21,7 @@ pub async fn all_handler( } pub async fn create_handler( - form: NewForm, + form: NewProject, db_access: impl DBProject, ) -> Result { match db_access.by_slug(&form.slug)? { @@ -35,7 +35,7 @@ pub async fn create_handler( pub async fn update_handler( id: i32, - form: UpdateForm, + form: UpdateProject, db_access: impl DBProject, ) -> Result { match db_access.by_id(id)? { diff --git a/src/api/projects/models.rs b/src/api/projects/models.rs index 954fcfc..cdd6bc2 100644 --- a/src/api/projects/models.rs +++ b/src/api/projects/models.rs @@ -32,7 +32,7 @@ pub struct QueryParams { #[derive(Insertable, Serialize, Deserialize, Debug)] #[diesel(table_name = projects)] -pub struct NewForm { +pub struct NewProject { pub name: String, pub slug: String, pub categories: Option>>, @@ -43,7 +43,7 @@ pub struct NewForm { #[derive(AsChangeset, Serialize, Deserialize, Debug)] #[diesel(table_name = projects)] -pub struct UpdateForm { +pub struct UpdateProject { pub name: Option, pub slug: Option, pub categories: Option>>, diff --git a/src/errors.rs b/src/errors.rs index c46db16..dc4e4be 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -4,11 +4,9 @@ use std::convert::Infallible; use warp::{hyper::StatusCode, Rejection, Reply}; use crate::{ - auth::errors::AuthenticationError, - db::errors::DBError, - // pagination::{PaginationError, SortError}, - // repository::{errors::RepositoryError, models::RepositorySortError}, - // user::{errors::UserError, models::UserSortError}, + api::issues::errors::IssueError, api::projects::errors::ProjectError, + api::repositories::errors::RepositoryError, api::users::errors::UserError, + auth::errors::AuthenticationError, db::errors::DBError, }; #[derive(Serialize, Deserialize, PartialEq, Debug)] @@ -17,53 +15,63 @@ pub struct ErrorResponse { } pub async fn error_handler(err: Rejection) -> std::result::Result { - let (status, message) = if err.is_not_found() { - (StatusCode::NOT_FOUND, "Resource not found".to_string()) - } else if err.find::().is_some() { - ( - StatusCode::METHOD_NOT_ALLOWED, - "Method not allowed".to_string(), - ) - } else if let Some(e) = err.find::() { - eprintln!("BodyDeserializeError error: {:?}", e); - (StatusCode::BAD_REQUEST, "Invalid request body".to_string()) - } else if let Some(e) = err.find::() { - eprintln!("InvalidQuery error: {:?}", e); - ( - StatusCode::BAD_REQUEST, - "Invalid query parameters".to_string(), - ) + eprintln!("error: {:?}", err); + if let Some(e) = err.find::() { + Ok(e.clone().into_response()) + } else if let Some(e) = err.find::() { + Ok(e.clone().into_response()) + } else if let Some(e) = err.find::() { + Ok(e.clone().into_response()) + } else if let Some(e) = err.find::() { + Ok(e.clone().into_response()) } else if let Some(e) = err.find::() { - eprintln!("AuthenticationError: {}", e.to_string()); - ( - StatusCode::UNAUTHORIZED, - format!("AuthenticationError - {}", e.to_string()), - ) - } else if let Some(db_error) = err.find::() { - match db_error { + Ok(e.clone().into_response()) + } else if let Some(e) = err.find::() { + let (code, message) = match e { DBError::DBPoolConnection(_) => ( StatusCode::INTERNAL_SERVER_ERROR, - "Database connection error".to_string(), + "Database connection error", ), - DBError::DBQuery(_) => (StatusCode::BAD_REQUEST, "Database query failed".to_string()), - DBError::ReadFile(_) => ( - StatusCode::INTERNAL_SERVER_ERROR, - "File read error".to_string(), - ), - DBError::DBTimeout(_) => ( - StatusCode::REQUEST_TIMEOUT, - "Database operation timed out".to_string(), - ), - } + DBError::DBQuery(_) => (StatusCode::BAD_REQUEST, "Database query failed"), + DBError::ReadFile(_) => (StatusCode::INTERNAL_SERVER_ERROR, "File read error"), + DBError::DBTimeout(_) => (StatusCode::REQUEST_TIMEOUT, "Database operation timed out"), + }; + + let json = warp::reply::json(&ErrorResponse { + message: message.to_string(), + }); + + Ok(warp::reply::with_status(json, code).into_response()) } else { - eprintln!("Unhandled error: {:?}", err); // Ensure all unexpected errors are logged. - ( - StatusCode::INTERNAL_SERVER_ERROR, - "Internal server error".to_string(), - ) - }; + let code; + let message; + + if err.is_not_found() { + // Handle not found errors + code = StatusCode::NOT_FOUND; + message = "Not Found"; + } else if err + .find::() + .is_some() + { + // Handle invalid body errors + code = StatusCode::BAD_REQUEST; + message = "Invalid Body"; + } else if err.find::().is_some() { + // Handle method not allowed errors + code = StatusCode::METHOD_NOT_ALLOWED; + message = "Method Not Allowed"; + } else { + // Handle all other errors + eprintln!("Unhandled error: {:?}", err); + code = StatusCode::INTERNAL_SERVER_ERROR; + message = "Internal Server Error"; + } - let json = warp::reply::json(&ErrorResponse { message }); + let json = warp::reply::json(&ErrorResponse { + message: message.into(), + }); - Ok(warp::reply::with_status(json, status)) + Ok(warp::reply::with_status(json, code).into_response()) + } } diff --git a/src/utils.rs b/src/utils.rs index c03aa6d..1999f13 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,7 +2,7 @@ use ::warp::Reply; use warp::{filters::BoxedFilter, Filter}; use crate::{ - api::{health, projects, repositories}, + api::{health, issues, projects, repositories, users}, db::{ self, errors::DBError, @@ -43,12 +43,14 @@ pub fn setup_filters(db: DBAccess) -> BoxedFilter<(impl Reply,)> { let health_route = health::routes::routes(db.clone()); let projects_route = projects::routes::routes(db.clone()); let repositories_route = repositories::routes::routes(db.clone()); - // let issues_route = issues::routes::routes(db.clone()); + let issues_route = issues::routes::routes(db.clone()); + let users_route = users::routes::routes(db.clone()); health_route .or(projects_route) .or(repositories_route) - // .or(issues_route) + .or(issues_route) + .or(users_route) .with(warp::cors().allow_any_origin()) .recover(error_handler) .boxed() From e6698dbcff02b2b3ae17a8842485bbbbc66e1781 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sat, 18 May 2024 18:50:04 -0300 Subject: [PATCH 32/98] fix: ci and lint --- .github/workflows/tests.yml | 11 ++++------- src/api/health/routes.rs | 6 ++---- src/api/repositories/db.rs | 4 ++-- src/utils.rs | 6 +++--- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 06a3fb8..cba6358 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,9 +6,6 @@ on: - main types: [opened, synchronize] -# env: -# RUSTFLAGS: "-Dwarnings" - jobs: build: runs-on: ubuntu-latest @@ -30,9 +27,9 @@ jobs: with: toolchain: stable - # - name: Run Clippy - # run: | - # cargo clippy --all-targets --all-features + - name: Run Clippy + run: | + cargo clippy --all-targets --all-features - name: Build code run: | @@ -40,7 +37,7 @@ jobs: - name: Test code run: | - cargo test --verbose + make test - name: Build docker image run: | diff --git a/src/api/health/routes.rs b/src/api/health/routes.rs index dfe8b6a..9fdddd9 100644 --- a/src/api/health/routes.rs +++ b/src/api/health/routes.rs @@ -5,10 +5,8 @@ use super::db::DBHealth; use super::handlers; pub fn routes(db_access: impl DBHealth) -> BoxedFilter<(impl Reply,)> { - let health_route = warp::path!("health") + warp::path!("health") .and(warp::any().map(move || db_access.clone())) .and_then(handlers::health_handler) - .boxed(); - - health_route + .boxed() } diff --git a/src/api/repositories/db.rs b/src/api/repositories/db.rs index 3913c0a..2290daf 100644 --- a/src/api/repositories/db.rs +++ b/src/api/repositories/db.rs @@ -35,13 +35,13 @@ impl DBRepository for DBAccess { if let Some(language_id) = params.language_ids { let ids: Vec = utils::parse_ids(&language_id); - if ids.len() > 0 { + if !ids.is_empty() { query = query.filter(repositories_dsl::language_id.eq_any(ids)); } } if let Some(project_id) = params.project_ids { let ids: Vec = utils::parse_ids(&project_id); - if ids.len() > 0 { + if !ids.is_empty() { query = query.filter(repositories_dsl::language_id.eq_any(ids)); } } diff --git a/src/utils.rs b/src/utils.rs index 1999f13..1bbfe40 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -11,8 +11,8 @@ use crate::{ errors::error_handler, }; -pub async fn setup_db(url: &String) -> DBAccess { - let db_pool = db::pool::create_db_pool(&url) +pub async fn setup_db(url: &str) -> DBAccess { + let db_pool = db::pool::create_db_pool(url) .map_err(DBError::DBPoolConnection) .expect("Failed to create DB pool"); @@ -67,5 +67,5 @@ pub fn parse_ids(s: &str) -> Vec { } pub fn parse_comma_values(s: &str) -> Vec { - s.split(",").map(|el: &str| el.to_string()).collect() + s.split(',').map(|el: &str| el.to_string()).collect() } From 0506831aec7de845316d3af864d0188ea5a5cf2e Mon Sep 17 00:00:00 2001 From: Leandro Date: Sat, 18 May 2024 18:54:13 -0300 Subject: [PATCH 33/98] fix: stage ci --- .github/workflows/stage.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stage.yml b/.github/workflows/stage.yml index 9219f15..0097e6d 100644 --- a/.github/workflows/stage.yml +++ b/.github/workflows/stage.yml @@ -26,10 +26,17 @@ jobs: with: toolchain: stable - - name: Build and test code + - name: Run Clippy + run: | + cargo clippy --all-targets --all-features + + - name: Build code run: | cargo build --verbose - cargo test --verbose + + - name: Test code + run: | + make test - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 From 8e6bdeee93c1979b96f89a4a1eba423dc8e1bc69 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 May 2024 21:56:18 +0000 Subject: [PATCH 34/98] chore(deps): bump mio from 0.8.9 to 0.8.11 Bumps [mio](https://github.com/tokio-rs/mio) from 0.8.9 to 0.8.11. - [Release notes](https://github.com/tokio-rs/mio/releases) - [Changelog](https://github.com/tokio-rs/mio/blob/master/CHANGELOG.md) - [Commits](https://github.com/tokio-rs/mio/compare/v0.8.9...v0.8.11) --- updated-dependencies: - dependency-name: mio dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 358b678..eb91ca0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -569,9 +569,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.9" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", From 0def61e23bbed84b19c964f129a5fabffb2940a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 May 2024 21:56:19 +0000 Subject: [PATCH 35/98] chore(deps): bump h2 from 0.3.22 to 0.3.26 Bumps [h2](https://github.com/hyperium/h2) from 0.3.22 to 0.3.26. - [Release notes](https://github.com/hyperium/h2/releases) - [Changelog](https://github.com/hyperium/h2/blob/v0.3.26/CHANGELOG.md) - [Commits](https://github.com/hyperium/h2/compare/v0.3.22...v0.3.26) --- updated-dependencies: - dependency-name: h2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 358b678..5cd48c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -327,9 +327,9 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "h2" -version = "0.3.22" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", From 377f68cee5e54bdbc98fc03bdda49a7b60b8f0fc Mon Sep 17 00:00:00 2001 From: Leandro Date: Sat, 18 May 2024 18:58:48 -0300 Subject: [PATCH 36/98] fix: test command --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2efa461..1335ad6 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ run: .PHONY: test test: - cargo test + DATABASE_URL="$(DATABASE_URL)" cargo test # DB From defc51010fb5620bc3a23791f693abf046637656 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sat, 18 May 2024 19:03:23 -0300 Subject: [PATCH 37/98] fix: test --- src/tests/health.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/health.rs b/src/tests/health.rs index 8302308..661a716 100644 --- a/src/tests/health.rs +++ b/src/tests/health.rs @@ -26,6 +26,7 @@ mod tests { } #[tokio::test] + #[ignore] async fn test_health_db() { let ApiConfig { database_url, .. } = ApiConfig::new(); let db = setup_db(&database_url).await; From 5547f79a01551f9dee5a621ad47afe5bd11e3f2b Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 19 May 2024 17:13:07 -0300 Subject: [PATCH 38/98] feat: integration tests enabler --- .github/workflows/tests.yml | 6 +++ .gitignore | 3 +- Cargo.lock | 86 +++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + Makefile | 2 +- src/tests/health.rs | 6 +-- src/utils.rs | 34 ++++++++++++++- 7 files changed, 131 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cba6358..d19f1f6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,8 +36,14 @@ jobs: cargo build --verbose - name: Test code + run: | + make testse + + - name: Integration tests run: | make test + env: + DATABASE_URL: ${{ secrets.TESTS_DATABASE_URL }} - name: Build docker image run: | diff --git a/.gitignore b/.gitignore index b5e703e..cb380dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target .vscode -.env \ No newline at end of file +.env +.test.env \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index eb91ca0..431f95b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -201,6 +201,17 @@ dependencies = [ "syn", ] +[[package]] +name = "diesel_migrations" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6036b3f0120c5961381b570ee20a02432d7e2d27ea60de9578799cf9156914ac" +dependencies = [ + "diesel", + "migrations_internals", + "migrations_macros", +] + [[package]] name = "diesel_table_macro_syntax" version = "0.1.0" @@ -503,7 +514,9 @@ dependencies = [ "base64 0.22.0", "chrono", "diesel", + "diesel_migrations", "dotenv", + "rand", "regex", "serde", "serde_derive", @@ -542,6 +555,27 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "migrations_internals" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada" +dependencies = [ + "serde", + "toml", +] + +[[package]] +name = "migrations_macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cce3325ac70e67bbab5bd837a31cae01f1a6db64e0e744a33cb03a543469ef08" +dependencies = [ + "migrations_internals", + "proc-macro2", + "quote", +] + [[package]] name = "mime" version = "0.3.17" @@ -876,6 +910,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1051,6 +1094,40 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -1363,3 +1440,12 @@ name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml index 37f8a40..316927e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,5 @@ base64 = "0.22.0" url = "2.5.0" diesel = { version = "2.1.5", features = ["postgres", "chrono", "r2d2"] } regex = "1.10.4" +rand = "0.8.5" +diesel_migrations = "2.1.0" diff --git a/Makefile b/Makefile index 1335ad6..ad68bfd 100644 --- a/Makefile +++ b/Makefile @@ -40,4 +40,4 @@ db-clean: .PHONY: test-db test-db: - cargo test -- --ignored --test-threads=1 + DATABASE_URL="$(DATABASE_URL)" cargo test -- --ignored --test-threads=1 diff --git a/src/tests/health.rs b/src/tests/health.rs index 661a716..3038a80 100644 --- a/src/tests/health.rs +++ b/src/tests/health.rs @@ -3,8 +3,7 @@ mod tests { use crate::{ api::health::{db::DBHealth, routes::routes}, db::errors::DBError, - types::ApiConfig, - utils::setup_db, + utils::generate_test_database, }; use warp::test::request; @@ -28,8 +27,7 @@ mod tests { #[tokio::test] #[ignore] async fn test_health_db() { - let ApiConfig { database_url, .. } = ApiConfig::new(); - let db = setup_db(&database_url).await; + let db = generate_test_database().await; let r = routes(db); let resp = request().path("/health").reply(&r).await; assert_eq!(resp.status(), 200); diff --git a/src/utils.rs b/src/utils.rs index 1bbfe40..303f944 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,4 @@ -use ::warp::Reply; -use warp::{filters::BoxedFilter, Filter}; +use std::env; use crate::{ api::{health, issues, projects, repositories, users}, @@ -10,6 +9,12 @@ use crate::{ }, errors::error_handler, }; +use ::warp::Reply; +use diesel::RunQueryDsl; +use diesel_migrations::MigrationHarness; +use diesel_migrations::{embed_migrations, EmbeddedMigrations}; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use warp::{filters::BoxedFilter, Filter}; pub async fn setup_db(url: &str) -> DBAccess { let db_pool = db::pool::create_db_pool(url) @@ -69,3 +74,28 @@ pub fn parse_ids(s: &str) -> Vec { pub fn parse_comma_values(s: &str) -> Vec { s.split(',').map(|el: &str| el.to_string()).collect() } + +pub fn generate_random_database_name() -> String { + let rng = thread_rng(); + let random_string: String = rng + .sample_iter(&Alphanumeric) + .map(char::from) + .take(10) + .collect(); + format!("test_db_{}", random_string) +} + +pub async fn generate_test_database() -> DBAccess { + const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); + let database_url = env::var("DATABASE_URL").expect("missing DATABASE"); + let database_name = generate_random_database_name(); + let db = setup_db(&database_url).await; + let conn = &mut db.get_db_conn(); + diesel::sql_query(format!("CREATE DATABASE {}", database_name)) + .execute(conn) + .expect("Failed to create database"); + db.get_db_conn() + .run_pending_migrations(MIGRATIONS) + .expect("Could not run migrations"); + db +} From 55f5ec970d028a7490652977cdad40a427d498eb Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 19 May 2024 17:16:16 -0300 Subject: [PATCH 39/98] fix: commands --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d19f1f6..b49e52d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,11 +37,11 @@ jobs: - name: Test code run: | - make testse + make test - name: Integration tests run: | - make test + make test-db env: DATABASE_URL: ${{ secrets.TESTS_DATABASE_URL }} From c8347eaf4427d00025d6c66fe1e87ccf367afb0b Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 19 May 2024 17:16:43 -0300 Subject: [PATCH 40/98] chore: remove command --- src/utils.rs | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 303f944..1e7700c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -20,27 +20,6 @@ pub async fn setup_db(url: &str) -> DBAccess { let db_pool = db::pool::create_db_pool(url) .map_err(DBError::DBPoolConnection) .expect("Failed to create DB pool"); - - // TODO: Extend this helper in tests by incorporating DB migration with Diesel - - // // In Cargo.toml - // [dependencies] - // diesel_migrations = "1.4.0" - - // // In main.rs - //#[macro_use] - // extern crate diesel_migrations; - - // embed_migrations!(); - - // // Get a database connection from the pool - // let conn = db_pool.get() - // .expect("Failed to get database connection from pool"); - - // // Run embedded migrations - // embedded_migrations::run(&conn) - // .expect("Failed to run database migrations"); - DBAccess::new(db_pool) } From c6016b5bf118c97f7eb4e2a9ce0d6541e0d622f8 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 19 May 2024 20:36:36 -0300 Subject: [PATCH 41/98] fix: dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f0743c4..b7143b6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1 ARG RUST_VERSION=1.74.0 -FROM rust:${RUST_VERSION}-slim-bullseye AS build +FROM rust:${RUST_VERSION} AS build RUN --mount=type=bind,source=src,target=src \ --mount=type=bind,source=Cargo.toml,target=Cargo.toml \ From 674a2d89a1b03c2894fe838146df986b7ec3c3d3 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 19 May 2024 20:36:56 -0300 Subject: [PATCH 42/98] chore: improve stage CI --- .github/workflows/stage.yml | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/workflows/stage.yml b/.github/workflows/stage.yml index 0097e6d..94d7803 100644 --- a/.github/workflows/stage.yml +++ b/.github/workflows/stage.yml @@ -38,16 +38,12 @@ jobs: run: | make test - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Integration tests + run: | + make test-db + env: + DATABASE_URL: ${{ secrets.TESTS_DATABASE_URL }} - - name: Build and push Docker image + - name: Build docker image run: | docker build -t ${{ vars.DOCKER_REGISTRY }}:${{ github.sha }} -f Dockerfile . - docker push ${{ vars.DOCKER_REGISTRY }}:${{ github.sha }} From 5f1aa248561f4e0d256451c4fced8652b5c07239 Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 19 May 2024 20:39:25 -0300 Subject: [PATCH 43/98] fix: improve db helper --- src/tests/utils.rs | 35 +++++++++++++++++++++++++++++++++++ src/utils.rs | 31 ------------------------------- 2 files changed, 35 insertions(+), 31 deletions(-) create mode 100644 src/tests/utils.rs diff --git a/src/tests/utils.rs b/src/tests/utils.rs new file mode 100644 index 0000000..6e41f91 --- /dev/null +++ b/src/tests/utils.rs @@ -0,0 +1,35 @@ +use std::env; + +use crate::{ + db::pool::{DBAccess, DBAccessor}, + utils::setup_db, +}; +use diesel::RunQueryDsl; +use diesel_migrations::MigrationHarness; +use diesel_migrations::{embed_migrations, EmbeddedMigrations}; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; + +pub fn generate_random_database_name() -> String { + let rng = thread_rng(); + let random_string: String = rng + .sample_iter(&Alphanumeric) + .map(char::from) + .take(10) + .collect(); + format!("test_db_{}", random_string) +} + +pub async fn generate_test_database() -> DBAccess { + const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); + let database_url = env::var("DATABASE_URL").expect("missing DATABASE"); + let database_name = generate_random_database_name(); + let db = setup_db(&database_url).await; + let conn = &mut db.get_db_conn(); + diesel::sql_query(format!("CREATE DATABASE {}", database_name)) + .execute(conn) + .expect("Failed to create database"); + db.get_db_conn() + .run_pending_migrations(MIGRATIONS) + .expect("Could not run migrations"); + db +} diff --git a/src/utils.rs b/src/utils.rs index 1e7700c..9f5fad8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,3 @@ -use std::env; - use crate::{ api::{health, issues, projects, repositories, users}, db::{ @@ -10,10 +8,6 @@ use crate::{ errors::error_handler, }; use ::warp::Reply; -use diesel::RunQueryDsl; -use diesel_migrations::MigrationHarness; -use diesel_migrations::{embed_migrations, EmbeddedMigrations}; -use rand::{distributions::Alphanumeric, thread_rng, Rng}; use warp::{filters::BoxedFilter, Filter}; pub async fn setup_db(url: &str) -> DBAccess { @@ -53,28 +47,3 @@ pub fn parse_ids(s: &str) -> Vec { pub fn parse_comma_values(s: &str) -> Vec { s.split(',').map(|el: &str| el.to_string()).collect() } - -pub fn generate_random_database_name() -> String { - let rng = thread_rng(); - let random_string: String = rng - .sample_iter(&Alphanumeric) - .map(char::from) - .take(10) - .collect(); - format!("test_db_{}", random_string) -} - -pub async fn generate_test_database() -> DBAccess { - const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); - let database_url = env::var("DATABASE_URL").expect("missing DATABASE"); - let database_name = generate_random_database_name(); - let db = setup_db(&database_url).await; - let conn = &mut db.get_db_conn(); - diesel::sql_query(format!("CREATE DATABASE {}", database_name)) - .execute(conn) - .expect("Failed to create database"); - db.get_db_conn() - .run_pending_migrations(MIGRATIONS) - .expect("Could not run migrations"); - db -} From 8d71cf285965d9267c8d18b513d32d04f301a0fb Mon Sep 17 00:00:00 2001 From: Leandro Date: Sun, 19 May 2024 20:39:38 -0300 Subject: [PATCH 44/98] fix: test --- src/tests/health.rs | 2 +- src/tests/mod.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tests/health.rs b/src/tests/health.rs index 3038a80..e882745 100644 --- a/src/tests/health.rs +++ b/src/tests/health.rs @@ -3,7 +3,7 @@ mod tests { use crate::{ api::health::{db::DBHealth, routes::routes}, db::errors::DBError, - utils::generate_test_database, + tests::utils::generate_test_database, }; use warp::test::request; diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 43a7c76..b26c5df 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1 +1,2 @@ pub mod health; +pub mod utils; From b404128d51de45bc6da06d72b69149e3d0161393 Mon Sep 17 00:00:00 2001 From: Igor Papandinas <26460174+ipapandinas@users.noreply.github.com> Date: Mon, 8 Jul 2024 12:55:41 +0200 Subject: [PATCH 45/98] feat: languages endpoint created --- migrations/2024-04-13-204151_init/up.sql | 4 +-- src/api/languages/db.rs | 42 ++++++++++++++++++++++++ src/api/languages/handlers.rs | 18 ++++++++++ src/api/languages/mod.rs | 3 ++ src/api/languages/models.rs | 14 ++++++-- src/api/languages/routes.rs | 32 ++++++++++++++++++ src/api/mod.rs | 2 +- src/schema.rs | 4 +-- src/utils.rs | 4 ++- 9 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 src/api/languages/db.rs create mode 100644 src/api/languages/handlers.rs create mode 100644 src/api/languages/routes.rs diff --git a/migrations/2024-04-13-204151_init/up.sql b/migrations/2024-04-13-204151_init/up.sql index 9480e8a..0ee2fdc 100644 --- a/migrations/2024-04-13-204151_init/up.sql +++ b/migrations/2024-04-13-204151_init/up.sql @@ -12,9 +12,9 @@ CREATE TABLE IF NOT EXISTS projects ( ); CREATE TABLE IF NOT EXISTS languages ( id SERIAL PRIMARY KEY, - name VARCHAR(255) UNIQUE, + slug VARCHAR(255) NOT NULL UNIQUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, - updated_at TIMESTAMP NULL + updated_at TIMESTAMP WITH TIME ZONE NULL ); -- basic github repository CREATE TABLE IF NOT EXISTS repositories ( diff --git a/src/api/languages/db.rs b/src/api/languages/db.rs new file mode 100644 index 0000000..b5f17d3 --- /dev/null +++ b/src/api/languages/db.rs @@ -0,0 +1,42 @@ +use diesel::prelude::*; + +use crate::db::{ + errors::DBError, + pool::{DBAccess, DBAccessor}, +}; + +use super::models::{Language, NewLanguage}; +use crate::schema::languages::dsl as languages_dsl; + +pub trait DBLanguage: Send + Sync + Clone + 'static { + fn all(&self) -> Result, DBError>; + fn create_or_get(&self, form: &NewLanguage) -> Result; +} + +impl DBLanguage for DBAccess { + fn all(&self) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + let result = languages_dsl::languages.load::(conn)?; + Ok(result) + } + + fn create_or_get(&self, form: &NewLanguage) -> Result { + let conn = &mut self.get_db_conn(); + + match languages_dsl::languages + .filter(languages_dsl::slug.eq(form.slug.clone())) + .first::(conn) + .optional()? + { + Some(language) => Ok(language), + None => { + let language = diesel::insert_into(languages_dsl::languages) + .values(form) + .get_result(conn) + .map_err(DBError::from)?; + + Ok(language) + } + } + } +} diff --git a/src/api/languages/handlers.rs b/src/api/languages/handlers.rs new file mode 100644 index 0000000..e99f355 --- /dev/null +++ b/src/api/languages/handlers.rs @@ -0,0 +1,18 @@ +use super::{db::DBLanguage, models::NewLanguage}; +use warp::{ + reject::Rejection, + reply::{json, Reply}, +}; + +pub async fn all_handler(db_access: impl DBLanguage) -> Result { + let languages = db_access.all()?; + Ok(json::>(&languages)) +} + +pub async fn create_handler( + form: NewLanguage, + db_access: impl DBLanguage, +) -> Result { + let language = db_access.create_or_get(&form)?; + Ok(json(&language)) +} diff --git a/src/api/languages/mod.rs b/src/api/languages/mod.rs index c446ac8..30d05c3 100644 --- a/src/api/languages/mod.rs +++ b/src/api/languages/mod.rs @@ -1 +1,4 @@ +pub mod db; +pub mod handlers; pub mod models; +pub mod routes; diff --git a/src/api/languages/models.rs b/src/api/languages/models.rs index 5016b20..f214d29 100644 --- a/src/api/languages/models.rs +++ b/src/api/languages/models.rs @@ -1,10 +1,20 @@ use crate::schema::languages; +use chrono::{DateTime, Utc}; use diesel::prelude::*; +use serde_derive::{Deserialize, Serialize}; -#[derive(Queryable, Identifiable, Selectable, Debug, PartialEq)] +#[derive(Queryable, Identifiable, Selectable, Debug, PartialEq, Serialize, Deserialize)] #[diesel(table_name = languages)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct Language { pub id: i32, - pub name: String, + pub slug: String, + pub created_at: DateTime, + pub updated_at: Option>, +} + +#[derive(Insertable, Serialize, Deserialize, Debug)] +#[diesel(table_name = languages)] +pub struct NewLanguage { + pub slug: String, } diff --git a/src/api/languages/routes.rs b/src/api/languages/routes.rs new file mode 100644 index 0000000..126d085 --- /dev/null +++ b/src/api/languages/routes.rs @@ -0,0 +1,32 @@ +use std::convert::Infallible; +use warp::filters::BoxedFilter; +use warp::{Filter, Reply}; + +use crate::auth::with_auth; + +use super::db::DBLanguage; +use super::handlers; + +fn with_db( + db_pool: impl DBLanguage, +) -> impl Filter + Clone { + warp::any().map(move || db_pool.clone()) +} + +pub fn routes(db_access: impl DBLanguage) -> BoxedFilter<(impl Reply,)> { + let language = warp::path!("languages"); + + let all_route = language + .and(warp::get()) + .and(with_db(db_access.clone())) + .and_then(handlers::all_handler); + + let create_route = language + .and(with_auth()) + .and(warp::post()) + .and(warp::body::json()) + .and(with_db(db_access.clone())) + .and_then(handlers::create_handler); + + all_route.or(create_route).boxed() +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 8e1a2b7..e9de5a5 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,6 +1,6 @@ pub mod health; // pub mod issues; -// pub mod languages; +pub mod languages; pub mod projects; pub mod repositories; pub mod users; diff --git a/src/schema.rs b/src/schema.rs index 6b9cfb2..f4189f7 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -22,9 +22,9 @@ diesel::table! { languages (id) { id -> Int4, #[max_length = 255] - name -> Nullable, + slug -> Varchar, created_at -> Timestamptz, - updated_at -> Nullable, + updated_at -> Nullable, } } diff --git a/src/utils.rs b/src/utils.rs index c03aa6d..28c54d3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,7 +2,7 @@ use ::warp::Reply; use warp::{filters::BoxedFilter, Filter}; use crate::{ - api::{health, projects, repositories}, + api::{health, languages, projects, repositories}, db::{ self, errors::DBError, @@ -43,11 +43,13 @@ pub fn setup_filters(db: DBAccess) -> BoxedFilter<(impl Reply,)> { let health_route = health::routes::routes(db.clone()); let projects_route = projects::routes::routes(db.clone()); let repositories_route = repositories::routes::routes(db.clone()); + let languages_route = languages::routes::routes(db.clone()); // let issues_route = issues::routes::routes(db.clone()); health_route .or(projects_route) .or(repositories_route) + .or(languages_route) // .or(issues_route) .with(warp::cors().allow_any_origin()) .recover(error_handler) From 595a76adb3e9edd8563cc386aefe7de2de47b803 Mon Sep 17 00:00:00 2001 From: Igor Papandinas <26460174+ipapandinas@users.noreply.github.com> Date: Mon, 8 Jul 2024 19:16:42 +0200 Subject: [PATCH 46/98] feat: repositories endpoint adjusted --- migrations/2024-04-13-204151_init/up.sql | 3 +- src/api/issues/db.rs | 39 ++++++++++++++++++++-- src/api/issues/models.rs | 6 ++++ src/api/languages/db.rs | 42 ------------------------ src/api/languages/handlers.rs | 18 ---------- src/api/languages/mod.rs | 4 --- src/api/languages/models.rs | 20 ----------- src/api/languages/routes.rs | 32 ------------------ src/api/mod.rs | 1 - src/api/projects/handlers.rs | 17 ++++++++-- src/api/repositories/db.rs | 23 +++++++++---- src/api/repositories/handlers.rs | 22 +++++++++++-- src/api/repositories/models.rs | 11 +++---- src/api/repositories/routes.rs | 29 +++++++++------- src/schema.rs | 4 +-- src/types.rs | 12 +++++-- src/utils.rs | 4 +-- 17 files changed, 127 insertions(+), 160 deletions(-) delete mode 100644 src/api/languages/db.rs delete mode 100644 src/api/languages/handlers.rs delete mode 100644 src/api/languages/mod.rs delete mode 100644 src/api/languages/models.rs delete mode 100644 src/api/languages/routes.rs diff --git a/migrations/2024-04-13-204151_init/up.sql b/migrations/2024-04-13-204151_init/up.sql index 0ee2fdc..e5bfbaa 100644 --- a/migrations/2024-04-13-204151_init/up.sql +++ b/migrations/2024-04-13-204151_init/up.sql @@ -20,8 +20,7 @@ CREATE TABLE IF NOT EXISTS languages ( CREATE TABLE IF NOT EXISTS repositories ( id SERIAL PRIMARY KEY, slug VARCHAR(255) NOT NULL UNIQUE, - name VARCHAR(255) NOT NULL, - language_id INT REFERENCES languages(id) NOT NULL, + language_slug VARCHAR(255) NOT NULL, project_id INT REFERENCES projects(id) NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NULL diff --git a/src/api/issues/db.rs b/src/api/issues/db.rs index c2f8bfc..8048ac6 100644 --- a/src/api/issues/db.rs +++ b/src/api/issues/db.rs @@ -27,7 +27,42 @@ impl DBIssue for DBAccess { pagination: PaginationParams, ) -> Result, DBError> { let conn = &mut self.get_db_conn(); - let mut query = issues_dsl::issues.into_boxed(); + let mut query = issues_dsl::issues + .inner_join( + repositories_dsl::repositories + .on(issues_dsl::repository_id.eq(repositories_dsl::id)), + ) + .inner_join( + projects_dsl::projects.on(repositories_dsl::project_id.eq(projects_dsl::id)), + ) + .left_join( + languages_dsl::languages.on(repositories_dsl::language_id.eq(languages_dsl::id)), + ) + .into_boxed(); + + if let Some(ref slug) = params.slug { + query = query.filter(projects::slug.eq(slug)); + } + + if let Some(ref category) = params.categories { + query = query.filter(projects::categories.contains(vec![category])); + } + + if let Some(ref purpose) = params.purposes { + query = query.filter(projects::purposes.contains(vec![purpose])); + } + + if let Some(ref stack_level) = params.stack_levels { + query = query.filter(projects::stack_levels.contains(vec![stack_level])); + } + + if let Some(ref technology) = params.technologies { + query = query.filter(projects::technologies.contains(vec![technology])); + } + + if let Some(language_id) = params.language_slug { + query = query.filter(languages::id.eq(language_id)); + } if let Some(raw_labels) = params.labels { let labels: Vec = utils::parse_comma_values(&raw_labels); @@ -48,7 +83,7 @@ impl DBIssue for DBAccess { query = query.offset(pagination.offset).limit(pagination.limit); - let result = query.load::(conn)?; + let result = query.select(issues::all_columns).load::(conn)?; Ok(result) } fn by_id(&self, id: i32) -> Result, DBError> { diff --git a/src/api/issues/models.rs b/src/api/issues/models.rs index 565ff84..92cbb67 100644 --- a/src/api/issues/models.rs +++ b/src/api/issues/models.rs @@ -53,7 +53,13 @@ pub struct UpdateIssue { #[derive(Deserialize, Debug)] pub struct QueryParams { + pub slug: Option, + pub categories: Option, + pub purposes: Option, + pub stack_levels: Option, + pub technologies: Option, pub labels: Option, + pub language_slug: Option, pub repository_id: Option, pub assignee_id: Option, pub open: Option, diff --git a/src/api/languages/db.rs b/src/api/languages/db.rs deleted file mode 100644 index b5f17d3..0000000 --- a/src/api/languages/db.rs +++ /dev/null @@ -1,42 +0,0 @@ -use diesel::prelude::*; - -use crate::db::{ - errors::DBError, - pool::{DBAccess, DBAccessor}, -}; - -use super::models::{Language, NewLanguage}; -use crate::schema::languages::dsl as languages_dsl; - -pub trait DBLanguage: Send + Sync + Clone + 'static { - fn all(&self) -> Result, DBError>; - fn create_or_get(&self, form: &NewLanguage) -> Result; -} - -impl DBLanguage for DBAccess { - fn all(&self) -> Result, DBError> { - let conn = &mut self.get_db_conn(); - let result = languages_dsl::languages.load::(conn)?; - Ok(result) - } - - fn create_or_get(&self, form: &NewLanguage) -> Result { - let conn = &mut self.get_db_conn(); - - match languages_dsl::languages - .filter(languages_dsl::slug.eq(form.slug.clone())) - .first::(conn) - .optional()? - { - Some(language) => Ok(language), - None => { - let language = diesel::insert_into(languages_dsl::languages) - .values(form) - .get_result(conn) - .map_err(DBError::from)?; - - Ok(language) - } - } - } -} diff --git a/src/api/languages/handlers.rs b/src/api/languages/handlers.rs deleted file mode 100644 index e99f355..0000000 --- a/src/api/languages/handlers.rs +++ /dev/null @@ -1,18 +0,0 @@ -use super::{db::DBLanguage, models::NewLanguage}; -use warp::{ - reject::Rejection, - reply::{json, Reply}, -}; - -pub async fn all_handler(db_access: impl DBLanguage) -> Result { - let languages = db_access.all()?; - Ok(json::>(&languages)) -} - -pub async fn create_handler( - form: NewLanguage, - db_access: impl DBLanguage, -) -> Result { - let language = db_access.create_or_get(&form)?; - Ok(json(&language)) -} diff --git a/src/api/languages/mod.rs b/src/api/languages/mod.rs deleted file mode 100644 index 30d05c3..0000000 --- a/src/api/languages/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod db; -pub mod handlers; -pub mod models; -pub mod routes; diff --git a/src/api/languages/models.rs b/src/api/languages/models.rs deleted file mode 100644 index f214d29..0000000 --- a/src/api/languages/models.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::schema::languages; -use chrono::{DateTime, Utc}; -use diesel::prelude::*; -use serde_derive::{Deserialize, Serialize}; - -#[derive(Queryable, Identifiable, Selectable, Debug, PartialEq, Serialize, Deserialize)] -#[diesel(table_name = languages)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct Language { - pub id: i32, - pub slug: String, - pub created_at: DateTime, - pub updated_at: Option>, -} - -#[derive(Insertable, Serialize, Deserialize, Debug)] -#[diesel(table_name = languages)] -pub struct NewLanguage { - pub slug: String, -} diff --git a/src/api/languages/routes.rs b/src/api/languages/routes.rs deleted file mode 100644 index 126d085..0000000 --- a/src/api/languages/routes.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::convert::Infallible; -use warp::filters::BoxedFilter; -use warp::{Filter, Reply}; - -use crate::auth::with_auth; - -use super::db::DBLanguage; -use super::handlers; - -fn with_db( - db_pool: impl DBLanguage, -) -> impl Filter + Clone { - warp::any().map(move || db_pool.clone()) -} - -pub fn routes(db_access: impl DBLanguage) -> BoxedFilter<(impl Reply,)> { - let language = warp::path!("languages"); - - let all_route = language - .and(warp::get()) - .and(with_db(db_access.clone())) - .and_then(handlers::all_handler); - - let create_route = language - .and(with_auth()) - .and(warp::post()) - .and(warp::body::json()) - .and(with_db(db_access.clone())) - .and_then(handlers::create_handler); - - all_route.or(create_route).boxed() -} diff --git a/src/api/mod.rs b/src/api/mod.rs index e9de5a5..feeb161 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,6 +1,5 @@ pub mod health; // pub mod issues; -pub mod languages; pub mod projects; pub mod repositories; pub mod users; diff --git a/src/api/projects/handlers.rs b/src/api/projects/handlers.rs index b2aa7ba..e2706f0 100644 --- a/src/api/projects/handlers.rs +++ b/src/api/projects/handlers.rs @@ -1,4 +1,4 @@ -use crate::types::PaginationParams; +use crate::types::{PaginatedResponse, PaginationParams}; use super::{ db::DBProject, @@ -16,8 +16,19 @@ pub async fn all_handler( params: QueryParams, pagination: PaginationParams, ) -> Result { - let projects = db_access.all(params, pagination)?; - Ok(json::>(&projects)) + let projects = db_access.all(params, pagination.clone())?; + let total_count = projects.len() as i64; + let has_next_page = pagination.offset + pagination.limit < total_count; + let has_previous_page = pagination.offset > 0; + + let response = PaginatedResponse { + total_count: Some(total_count), + has_next_page, + has_previous_page, + data: projects, + }; + + Ok(json(&response)) } pub async fn create_handler( diff --git a/src/api/repositories/db.rs b/src/api/repositories/db.rs index 3913c0a..1f41b6b 100644 --- a/src/api/repositories/db.rs +++ b/src/api/repositories/db.rs @@ -1,3 +1,5 @@ +use diesel::dsl::sql; +use diesel::sql_types::Text; use diesel::{dsl::now, prelude::*}; use super::models::{NewRepository, QueryParams, Repository, UpdateRepository}; @@ -22,6 +24,7 @@ pub trait DBRepository: Send + Sync + Clone + 'static { fn update(&self, id: i32, repo: &UpdateRepository) -> Result; fn delete(&self, id: i32) -> Result<(), DBError>; fn by_slug(&self, slug: &str) -> Result, DBError>; + fn aggregate_languages(&self) -> Result, DBError>; } impl DBRepository for DBAccess { @@ -33,16 +36,14 @@ impl DBRepository for DBAccess { let conn = &mut self.get_db_conn(); let mut query = repositories_dsl::repositories.into_boxed(); - if let Some(language_id) = params.language_ids { - let ids: Vec = utils::parse_ids(&language_id); - if ids.len() > 0 { - query = query.filter(repositories_dsl::language_id.eq_any(ids)); - } + if let Some(raw_languages) = params.languages { + let languages: Vec = utils::parse_comma_values(&raw_languages); + query = query.filter(repositories_dsl::language_slug.eq_any(languages)); } if let Some(project_id) = params.project_ids { let ids: Vec = utils::parse_ids(&project_id); if ids.len() > 0 { - query = query.filter(repositories_dsl::language_id.eq_any(ids)); + query = query.filter(repositories_dsl::project_id.eq_any(ids)); } } @@ -86,6 +87,7 @@ impl DBRepository for DBAccess { Ok(project) } + fn delete(&self, id: i32) -> Result<(), DBError> { let conn = &mut self.get_db_conn(); diesel::delete(repositories_dsl::repositories.filter(repositories_dsl::id.eq(id))) @@ -94,6 +96,7 @@ impl DBRepository for DBAccess { Ok(()) } + fn by_slug(&self, slug: &str) -> Result, DBError> { let conn = &mut self.get_db_conn(); @@ -105,4 +108,12 @@ impl DBRepository for DBAccess { Ok(result) } + + fn aggregate_languages(&self) -> Result, DBError> { + let conn = &mut self.get_db_conn(); + let languages = repositories_dsl::repositories + .select(sql::("DISTINCT language_slug")) + .load::(conn)?; + Ok(languages) + } } diff --git a/src/api/repositories/handlers.rs b/src/api/repositories/handlers.rs index e40fb2e..6fe8523 100644 --- a/src/api/repositories/handlers.rs +++ b/src/api/repositories/handlers.rs @@ -4,7 +4,7 @@ use warp::{ reply::{json, with_status, Reply}, }; -use crate::types::PaginationParams; +use crate::types::{PaginatedResponse, PaginationParams}; use super::{ db::DBRepository, @@ -24,8 +24,19 @@ pub async fn all_handler( params: QueryParams, pagination: PaginationParams, ) -> Result { - let repositories = db_access.all(params, pagination)?; - Ok(json::>(&repositories)) + let repositories = db_access.all(params, pagination.clone())?; + let total_count = repositories.len() as i64; + let has_next_page = pagination.offset + pagination.limit < total_count; + let has_previous_page = pagination.offset > 0; + + let response = PaginatedResponse { + total_count: Some(total_count), + has_next_page, + has_previous_page, + data: repositories, + }; + + Ok(json(&response)) } pub async fn create_handler( @@ -63,3 +74,8 @@ pub async fn delete_handler( None => Err(warp::reject::custom(RepositoryError::NotFound(id))), } } + +pub async fn get_languages_handler(db_access: impl DBRepository) -> Result { + let languages = db_access.aggregate_languages()?; + Ok(json(&languages)) +} diff --git a/src/api/repositories/models.rs b/src/api/repositories/models.rs index 126f7bb..25f6a92 100644 --- a/src/api/repositories/models.rs +++ b/src/api/repositories/models.rs @@ -11,9 +11,8 @@ use serde_derive::{Deserialize, Serialize}; #[diesel(check_for_backend(diesel::pg::Pg))] pub struct Repository { pub id: i32, - pub name: String, pub slug: String, - pub language_id: i32, + pub language_slug: String, pub project_id: i32, pub created_at: DateTime, pub updated_at: Option>, @@ -23,24 +22,22 @@ pub struct Repository { pub struct QueryParams { pub slug: Option, pub name: Option, - pub language_ids: Option, + pub languages: Option, pub project_ids: Option, } #[derive(Insertable, Serialize, Deserialize, Debug)] #[diesel(table_name = repositories)] pub struct NewRepository { - pub name: String, pub slug: String, - pub language_id: i32, + pub language_slug: String, pub project_id: i32, } #[derive(AsChangeset, Serialize, Deserialize, Debug)] #[diesel(table_name = repositories)] pub struct UpdateRepository { - pub name: Option, pub slug: Option, - pub language_id: Option, + pub language_slug: Option, pub project_id: Option, } diff --git a/src/api/repositories/routes.rs b/src/api/repositories/routes.rs index 4677b33..bc7e709 100644 --- a/src/api/repositories/routes.rs +++ b/src/api/repositories/routes.rs @@ -19,46 +19,51 @@ fn with_db( } pub fn routes(db_access: impl DBRepository) -> BoxedFilter<(impl Reply,)> { - let repository = warp::path!("repositories"); // TODO: move this to the "organization" endpoint as a subendpoint + let repository = warp::path!("repositories"); let repository_id = warp::path!("repositories" / i32); - // let repository_name = warp::path!("repositories" / "name" / String); - let get_repositories = repository + let all_route = repository .and(warp::get()) .and(with_db(db_access.clone())) .and(warp::query::()) .and(warp::query::()) .and_then(handlers::all_handler); - let get_repository = repository_id + let by_id_route = repository_id .and(warp::get()) .and(with_db(db_access.clone())) .and_then(handlers::by_id); - let create_repository = repository + let create_route = repository .and(with_auth()) .and(warp::post()) .and(warp::body::json()) .and(with_db(db_access.clone())) .and_then(handlers::create_handler); - let update_repository = repository_id + let update_route = repository_id .and(with_auth()) .and(warp::patch()) .and(warp::body::json()) .and(with_db(db_access.clone())) .and_then(handlers::update_handler); - let delete_repository = repository_id + let delete_route = repository_id .and(with_auth()) .and(warp::delete()) .and(with_db(db_access.clone())) .and_then(handlers::delete_handler); - get_repositories - .or(get_repository) - .or(create_repository) - .or(delete_repository) - .or(update_repository) + let languages_route = warp::path!("languages") + .and(warp::get()) + .and(with_db(db_access.clone())) + .and_then(handlers::get_languages_handler); + + all_route + .or(by_id_route) + .or(create_route) + .or(update_route) + .or(delete_route) + .or(languages_route) .boxed() } diff --git a/src/schema.rs b/src/schema.rs index f4189f7..5feab7e 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -50,8 +50,7 @@ diesel::table! { #[max_length = 255] slug -> Varchar, #[max_length = 255] - name -> Varchar, - language_id -> Int4, + language_slug -> Varchar, project_id -> Int4, created_at -> Timestamptz, updated_at -> Nullable, @@ -70,7 +69,6 @@ diesel::table! { diesel::joinable!(issues -> repositories (repository_id)); diesel::joinable!(issues -> users (assignee_id)); -diesel::joinable!(repositories -> languages (language_id)); diesel::joinable!(repositories -> projects (project_id)); diesel::allow_tables_to_appear_in_same_query!( diff --git a/src/types.rs b/src/types.rs index 9d50fe3..81a0652 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,5 +1,5 @@ use dotenv::dotenv; -use serde::Deserialize; +use serde_derive::{Deserialize, Serialize}; use std::env; /// Configuration used by this API. @@ -27,7 +27,7 @@ impl ApiConfig { } } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct PaginationParams { #[serde(default = "default_limit")] pub limit: i64, @@ -42,3 +42,11 @@ fn default_limit() -> i64 { fn default_offset() -> i64 { 0 // Default offset } + +#[derive(Serialize)] +pub struct PaginatedResponse { + pub total_count: Option, + pub has_next_page: bool, + pub has_previous_page: bool, + pub data: Vec, +} diff --git a/src/utils.rs b/src/utils.rs index 28c54d3..c03aa6d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,7 +2,7 @@ use ::warp::Reply; use warp::{filters::BoxedFilter, Filter}; use crate::{ - api::{health, languages, projects, repositories}, + api::{health, projects, repositories}, db::{ self, errors::DBError, @@ -43,13 +43,11 @@ pub fn setup_filters(db: DBAccess) -> BoxedFilter<(impl Reply,)> { let health_route = health::routes::routes(db.clone()); let projects_route = projects::routes::routes(db.clone()); let repositories_route = repositories::routes::routes(db.clone()); - let languages_route = languages::routes::routes(db.clone()); // let issues_route = issues::routes::routes(db.clone()); health_route .or(projects_route) .or(repositories_route) - .or(languages_route) // .or(issues_route) .with(warp::cors().allow_any_origin()) .recover(error_handler) From e3c8585184bfc96b7d7e92ca44a044b00cfef6f4 Mon Sep 17 00:00:00 2001 From: kudos Date: Wed, 7 Aug 2024 21:54:19 -0300 Subject: [PATCH 47/98] chore: push image to private ECR --- .github/workflows/prod.yml | 51 +++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 69b2aa2..59dd8c2 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -1,8 +1,22 @@ name: Production +# on: +# release: +# types: [created] + + on: - release: - types: [created] + push: + branches: + - chore/ecs + + +env: + AWS_REGION: us-east-1 + ECR_REPOSITORY: issues-api +permissions: + id-token: write + contents: read jobs: build: @@ -15,13 +29,32 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - - name: Log in to Docker Hub - uses: docker/login-action@v3 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + role-to-assume: ${{ secrets.ECR_ROLE }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Amazon ECR Private + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 - - name: Build and push Docker image + - name: Build, tag, and push docker image to Amazon ECR + env: + REGISTRY: ${{ steps.login-ecr.outputs.registry }} + REPOSITORY: issues-api run: | - docker build -t ${{ vars.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} -f Dockerfile . - docker push ${{ vars.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} + docker build -t $REGISTRY/${{ env.ECR_REPOSITORY }}:${{ github.event.release.tag_name }} . + docker push $REGISTRY/${{ env.ECR_REPOSITORY }}:${{ github.event.release.tag_name }} + + # - name: Log in to Docker Hub + # uses: docker/login-action@v3 + # with: + # username: ${{ secrets.DOCKERHUB_USERNAME }} + # password: ${{ secrets.DOCKERHUB_TOKEN }} + + # - name: Build and push Docker image + # run: | + # docker build -t ${{ vars.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} -f Dockerfile . + # docker push ${{ vars.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} From 612960d2d47812353e83df0fd6a9e5a1d8532547 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Wed, 7 Aug 2024 21:59:21 -0300 Subject: [PATCH 48/98] chore: change tag --- .github/workflows/prod.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 59dd8c2..a9044b0 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -45,7 +45,8 @@ jobs: REGISTRY: ${{ steps.login-ecr.outputs.registry }} REPOSITORY: issues-api run: | - docker build -t $REGISTRY/${{ env.ECR_REPOSITORY }}:${{ github.event.release.tag_name }} . + # docker build -t $REGISTRY/${{ env.ECR_REPOSITORY }}:${{ github.event.release.tag_name }} . + docker build -t $REGISTRY/${{ env.ECR_REPOSITORY }}:${{ github.sha }} . docker push $REGISTRY/${{ env.ECR_REPOSITORY }}:${{ github.event.release.tag_name }} # - name: Log in to Docker Hub From 3bb57778b178377501fd5c7b8075c220f67a5908 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Wed, 7 Aug 2024 22:03:30 -0300 Subject: [PATCH 49/98] chore: fix name --- .github/workflows/prod.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index a9044b0..1ec5191 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -45,9 +45,10 @@ jobs: REGISTRY: ${{ steps.login-ecr.outputs.registry }} REPOSITORY: issues-api run: | - # docker build -t $REGISTRY/${{ env.ECR_REPOSITORY }}:${{ github.event.release.tag_name }} . - docker build -t $REGISTRY/${{ env.ECR_REPOSITORY }}:${{ github.sha }} . - docker push $REGISTRY/${{ env.ECR_REPOSITORY }}:${{ github.event.release.tag_name }} + image=$REGISTRY/${{ env.ECR_REPOSITORY }}:${{ github.event.release.tag_name }} + image=$REGISTRY/${{ env.ECR_REPOSITORY }}:${{ github.sha }} + docker build -t $image . + docker push $image # - name: Log in to Docker Hub # uses: docker/login-action@v3 From e299873e277f34a2b41161ae03c5329c1bada572 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Thu, 8 Aug 2024 19:13:37 -0300 Subject: [PATCH 50/98] chore: fix dockerfile --- .github/workflows/prod.yml | 12 ++++++++---- Dockerfile | 13 ++++++++++--- Makefile | 5 +++++ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 1ec5191..a748755 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -1,10 +1,11 @@ name: Production -# on: +# TODO: restore +# on: # release: # types: [created] - +# TODO: delete on: push: branches: @@ -46,10 +47,11 @@ jobs: REPOSITORY: issues-api run: | image=$REGISTRY/${{ env.ECR_REPOSITORY }}:${{ github.event.release.tag_name }} - image=$REGISTRY/${{ env.ECR_REPOSITORY }}:${{ github.sha }} + image=$REGISTRY/${{ env.ECR_REPOSITORY }}:${{ github.sha }} # TODO: delete docker build -t $image . docker push $image - + + # TODO: restore? # - name: Log in to Docker Hub # uses: docker/login-action@v3 # with: @@ -58,5 +60,7 @@ jobs: # - name: Build and push Docker image # run: | + # image=${{ vars.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} + # image=${{ vars.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} # TODO: delete # docker build -t ${{ vars.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} -f Dockerfile . # docker push ${{ vars.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} diff --git a/Dockerfile b/Dockerfile index b7143b6..610ad7a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,17 +8,23 @@ RUN --mount=type=bind,source=src,target=src \ --mount=type=bind,source=Cargo.lock,target=Cargo.lock \ --mount=type=cache,target=/app/target/ \ --mount=type=cache,target=/usr/local/cargo/registry/ \ - # --mount=type=bind,source=migrations,target=migrations \ < Date: Thu, 8 Aug 2024 20:59:29 -0300 Subject: [PATCH 51/98] chore: automate deployment --- .github/workflows/prod.yml | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index a748755..d65e780 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -15,6 +15,7 @@ on: env: AWS_REGION: us-east-1 ECR_REPOSITORY: issues-api + permissions: id-token: write contents: read @@ -42,6 +43,7 @@ jobs: uses: aws-actions/amazon-ecr-login@v2 - name: Build, tag, and push docker image to Amazon ECR + id: build-image env: REGISTRY: ${{ steps.login-ecr.outputs.registry }} REPOSITORY: issues-api @@ -50,7 +52,27 @@ jobs: image=$REGISTRY/${{ env.ECR_REPOSITORY }}:${{ github.sha }} # TODO: delete docker build -t $image . docker push $image - + echo "image=$image >> $GITHUB_OUTPUT + + - name: Download task definition + run: | + aws ecs describe-task-definition --task-definition issues-api --query taskDefinition > task-definition.json + + - name: Fill in the new image ID in the Amazon ECS task definition + id: task-def + uses: aws-actions/amazon-ecs-render-task-definition@v1 + with: + task-definition: task-definition.json + container-name: issues-api + image: ${{ steps.build-image.outputs.image }} + + - name: Deploy Amazon ECS task definition + uses: aws-actions/amazon-ecs-deploy-task-definition@v2 + with: + task-definition: ${{ steps.task-def.outputs.task-definition }} + service: issues-api + cluster: production + # wait-for-service-stability: true # TODO: restore? # - name: Log in to Docker Hub # uses: docker/login-action@v3 From 0f7d5ad150ef709a40a86e9b4cb5e9428a2614be Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Thu, 8 Aug 2024 21:11:52 -0300 Subject: [PATCH 52/98] chore: fix var --- .github/workflows/prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index d65e780..e58509d 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -52,7 +52,7 @@ jobs: image=$REGISTRY/${{ env.ECR_REPOSITORY }}:${{ github.sha }} # TODO: delete docker build -t $image . docker push $image - echo "image=$image >> $GITHUB_OUTPUT + echo "image=$image" >> $GITHUB_OUTPUT - name: Download task definition run: | From 5a296a123e038684e0e78e2a66cfb5a745da6025 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Thu, 8 Aug 2024 21:26:45 -0300 Subject: [PATCH 53/98] chore: retry --- .github/workflows/prod.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index e58509d..1433843 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -73,6 +73,7 @@ jobs: service: issues-api cluster: production # wait-for-service-stability: true + # TODO: restore? # - name: Log in to Docker Hub # uses: docker/login-action@v3 From 59b1a7ef56c85f5e4906f4a430dbd423867def5a Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Thu, 8 Aug 2024 21:30:51 -0300 Subject: [PATCH 54/98] chore: retry --- .github/workflows/prod.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 1433843..28b24b3 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -75,6 +75,7 @@ jobs: # wait-for-service-stability: true # TODO: restore? + # - name: Log in to Docker Hub # uses: docker/login-action@v3 # with: From 9d5d5b33722da9531af661ace1638403b4901623 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sat, 10 Aug 2024 18:08:27 -0300 Subject: [PATCH 55/98] chore: use vars --- .github/workflows/prod.yml | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 28b24b3..026bb11 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -11,10 +11,13 @@ on: branches: - chore/ecs - env: AWS_REGION: us-east-1 ECR_REPOSITORY: issues-api + ECS_CLUSTER: production + ECS_SERVICE: issues-api + ECS_CONTAINER: issues-api + TASK_FILE: task.json permissions: id-token: write @@ -31,7 +34,6 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: @@ -56,35 +58,20 @@ jobs: - name: Download task definition run: | - aws ecs describe-task-definition --task-definition issues-api --query taskDefinition > task-definition.json + aws ecs describe-task-definition --task-definition issues-api --query taskDefinition > ${{ env.TASK_FILE }} - name: Fill in the new image ID in the Amazon ECS task definition id: task-def uses: aws-actions/amazon-ecs-render-task-definition@v1 with: - task-definition: task-definition.json - container-name: issues-api + task-definition: ${{ env.TASK_FILE }} + container-name: ${{ env.ECS_CONTAINER }} image: ${{ steps.build-image.outputs.image }} - name: Deploy Amazon ECS task definition uses: aws-actions/amazon-ecs-deploy-task-definition@v2 with: task-definition: ${{ steps.task-def.outputs.task-definition }} - service: issues-api - cluster: production - # wait-for-service-stability: true - - # TODO: restore? - - # - name: Log in to Docker Hub - # uses: docker/login-action@v3 - # with: - # username: ${{ secrets.DOCKERHUB_USERNAME }} - # password: ${{ secrets.DOCKERHUB_TOKEN }} - - # - name: Build and push Docker image - # run: | - # image=${{ vars.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} - # image=${{ vars.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} # TODO: delete - # docker build -t ${{ vars.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} -f Dockerfile . - # docker push ${{ vars.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} + service: ${{ env.ECS_SERVICE }} + cluster: ${{ env.ECS_CLUSTER }} + wait-for-service-stability: true \ No newline at end of file From 4081ca636a1b5a90fa5d6e39a4b089f6950e8484 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sat, 10 Aug 2024 18:36:44 -0300 Subject: [PATCH 56/98] chore: remove param --- .github/workflows/prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 026bb11..0146a40 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -74,4 +74,4 @@ jobs: task-definition: ${{ steps.task-def.outputs.task-definition }} service: ${{ env.ECS_SERVICE }} cluster: ${{ env.ECS_CLUSTER }} - wait-for-service-stability: true \ No newline at end of file + # wait-for-service-stability: true \ No newline at end of file From d11fdd96869e904606cac40fa00788096e63959c Mon Sep 17 00:00:00 2001 From: Igor Papandinas <26460174+ipapandinas@users.noreply.github.com> Date: Sat, 10 Aug 2024 23:50:41 +0200 Subject: [PATCH 57/98] issues - still WIP --- Makefile | 2 +- src/api/issues/db.rs | 139 ++++++++++++++++++++--------------- src/api/issues/handlers.rs | 19 ++++- src/api/issues/models.rs | 18 ++--- src/api/mod.rs | 2 +- src/api/projects/db.rs | 51 +++++++------ src/api/projects/handlers.rs | 3 +- src/utils.rs | 6 +- 8 files changed, 139 insertions(+), 101 deletions(-) diff --git a/Makefile b/Makefile index 2efa461..446c550 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ PORT?=8000 USERNAME?=test PASSWORD?=test DOCKER_DB_CONTAINER_NAME:=db -DOCKER_COMPOSE:=docker-compose +DOCKER_COMPOSE:=docker compose DOCKER_COMPOSE_FILE:=docker-compose.yaml # API diff --git a/src/api/issues/db.rs b/src/api/issues/db.rs index 8048ac6..a7ac76d 100644 --- a/src/api/issues/db.rs +++ b/src/api/issues/db.rs @@ -3,6 +3,9 @@ use diesel::prelude::*; use super::models::{Issue, NewIssue, QueryParams, UpdateIssue}; use crate::schema::issues::dsl as issues_dsl; +use crate::schema::languages::dsl as languages_dsl; +use crate::schema::projects::dsl as projects_dsl; +use crate::schema::repositories::dsl as repositories_dsl; use crate::db::{ errors::DBError, @@ -11,8 +14,11 @@ use crate::db::{ use crate::types::PaginationParams; use crate::utils; pub trait DBIssue: Send + Sync + Clone + 'static { - fn all(&self, params: QueryParams, pagination: PaginationParams) - -> Result, DBError>; + fn all( + &self, + params: QueryParams, + pagination: PaginationParams, + ) -> Result<(Vec, i64), DBError>; fn by_id(&self, id: i32) -> Result, DBError>; fn by_number(&self, repository_id: i32, number: i32) -> Result, DBError>; fn create(&self, issue: &NewIssue) -> Result; @@ -25,66 +31,77 @@ impl DBIssue for DBAccess { &self, params: QueryParams, pagination: PaginationParams, - ) -> Result, DBError> { + ) -> Result<(Vec, i64), DBError> { let conn = &mut self.get_db_conn(); - let mut query = issues_dsl::issues - .inner_join( - repositories_dsl::repositories - .on(issues_dsl::repository_id.eq(repositories_dsl::id)), - ) - .inner_join( - projects_dsl::projects.on(repositories_dsl::project_id.eq(projects_dsl::id)), - ) - .left_join( - languages_dsl::languages.on(repositories_dsl::language_id.eq(languages_dsl::id)), - ) - .into_boxed(); - - if let Some(ref slug) = params.slug { - query = query.filter(projects::slug.eq(slug)); - } - - if let Some(ref category) = params.categories { - query = query.filter(projects::categories.contains(vec![category])); - } - - if let Some(ref purpose) = params.purposes { - query = query.filter(projects::purposes.contains(vec![purpose])); - } - - if let Some(ref stack_level) = params.stack_levels { - query = query.filter(projects::stack_levels.contains(vec![stack_level])); - } - - if let Some(ref technology) = params.technologies { - query = query.filter(projects::technologies.contains(vec![technology])); - } - - if let Some(language_id) = params.language_slug { - query = query.filter(languages::id.eq(language_id)); - } - - if let Some(raw_labels) = params.labels { - let labels: Vec = utils::parse_comma_values(&raw_labels); - query = query.filter(issues_dsl::labels.overlaps_with(labels)); - } - - if let Some(open) = params.open { - query = query.filter(issues_dsl::open.eq(open)); - } - - if let Some(assignee_id) = params.assignee_id { - query = query.filter(issues_dsl::assignee_id.eq(assignee_id)); - } - - if let Some(repository_id) = params.repository_id { - query = query.filter(issues_dsl::repository_id.eq(repository_id)); - } - - query = query.offset(pagination.offset).limit(pagination.limit); - - let result = query.select(issues::all_columns).load::(conn)?; - Ok(result) + + let build_query = || { + let mut query = issues_dsl::issues + .inner_join( + repositories_dsl::repositories + .on(issues_dsl::repository_id.eq(repositories_dsl::id)), + ) + .inner_join( + projects_dsl::projects.on(repositories_dsl::project_id.eq(projects_dsl::id)), + ) + .left_join( + languages_dsl::languages + .on(repositories_dsl::language_slug.eq(languages_dsl::slug)), + ) + .into_boxed(); + + if let Some(slug) = params.slug.as_ref() { + query = query.filter(projects_dsl::slug.eq(slug)); + } + + if let Some(category) = params.categories.as_ref() { + query = query.filter(projects_dsl::categories.contains(vec![category])); + } + + if let Some(purpose) = params.purposes.as_ref() { + query = query.filter(projects_dsl::purposes.contains(vec![purpose])); + } + + if let Some(stack_level) = params.stack_levels.as_ref() { + query = query.filter(projects_dsl::stack_levels.contains(vec![stack_level])); + } + + if let Some(technology) = params.technologies.as_ref() { + query = query.filter(projects_dsl::technologies.contains(vec![technology])); + } + + if let Some(language_slug) = params.language_slug.as_ref() { + query = query.filter(languages_dsl::slug.eq(language_slug)); + } + + if let Some(raw_labels) = params.labels.as_ref() { + let labels: Vec = utils::parse_comma_values(&raw_labels); + query = query.filter(issues_dsl::labels.overlaps_with(labels)); + } + + if let Some(open) = params.open.as_ref() { + query = query.filter(issues_dsl::open.eq(open)); + } + + if let Some(assignee_id) = params.assignee_id.as_ref() { + query = query.filter(issues_dsl::assignee_id.eq(assignee_id)); + } + + if let Some(repository_id) = params.repository_id.as_ref() { + query = query.filter(issues_dsl::repository_id.eq(repository_id)); + } + + query + }; + + let total_count = build_query().count().get_result::(conn)?; + + let result = build_query() + .offset(pagination.offset) + .limit(pagination.limit) + .select(issues_dsl::issues::all_columns()) + .load::(conn)?; + + Ok((result, total_count)) } fn by_id(&self, id: i32) -> Result, DBError> { let conn = &mut self.get_db_conn(); diff --git a/src/api/issues/handlers.rs b/src/api/issues/handlers.rs index afc6a55..bc58ce9 100644 --- a/src/api/issues/handlers.rs +++ b/src/api/issues/handlers.rs @@ -4,7 +4,10 @@ use warp::{ reply::{json, with_status, Reply}, }; -use crate::{api::repositories::db::DBRepository, types::PaginationParams}; +use crate::{ + api::repositories::db::DBRepository, + types::{PaginatedResponse, PaginationParams}, +}; use super::{ db::DBIssue, @@ -24,8 +27,18 @@ pub async fn all_handler( params: QueryParams, pagination: PaginationParams, ) -> Result { - let issues = db_access.all(params, pagination)?; - Ok(json::>(&issues)) + let (issues, total_count) = db_access.all(params, pagination.clone())?; + let has_next_page = pagination.offset + pagination.limit < total_count; + let has_previous_page = pagination.offset > 0; + + let response = PaginatedResponse { + total_count: Some(total_count), + has_next_page, + has_previous_page, + data: issues, + }; + + Ok(json(&response)) } pub async fn create_handler( diff --git a/src/api/issues/models.rs b/src/api/issues/models.rs index 92cbb67..7e65049 100644 --- a/src/api/issues/models.rs +++ b/src/api/issues/models.rs @@ -12,15 +12,15 @@ use serde_derive::{Deserialize, Serialize}; pub struct Issue { pub id: i32, pub number: i32, - // pub title: String, - // pub labels: Option>>, - // pub open: bool, - // pub repository_id: i32, - // pub assignee_id: Option, - // pub e_tag: String, - // pub issue_created_at: DateTime, - // pub created_at: DateTime, - // pub updated_at: Option>, + pub title: String, + pub labels: Option>>, + pub open: bool, + pub assignee_id: Option, + pub e_tag: String, + pub repository_id: i32, + pub issue_created_at: DateTime, + pub created_at: DateTime, + pub updated_at: Option>, } #[derive(Insertable, Serialize, Deserialize, Debug)] diff --git a/src/api/mod.rs b/src/api/mod.rs index feeb161..3781899 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,5 +1,5 @@ pub mod health; -// pub mod issues; +pub mod issues; pub mod projects; pub mod repositories; pub mod users; diff --git a/src/api/projects/db.rs b/src/api/projects/db.rs index 6e09950..7242be3 100644 --- a/src/api/projects/db.rs +++ b/src/api/projects/db.rs @@ -16,7 +16,7 @@ pub trait DBProject: Send + Sync + Clone + 'static { &self, params: QueryParams, pagination: PaginationParams, - ) -> Result, DBError>; + ) -> Result<(Vec, i64), DBError>; fn by_id(&self, id: i32) -> Result, DBError>; fn by_slug(&self, slug: &str) -> Result, DBError>; fn create(&self, form: &NewForm) -> Result; @@ -29,33 +29,42 @@ impl DBProject for DBAccess { &self, params: QueryParams, pagination: PaginationParams, - ) -> Result, DBError> { + ) -> Result<(Vec, i64), DBError> { let conn = &mut self.get_db_conn(); - let mut query = projects_dsl::projects.into_boxed(); - if let Some(slug) = params.slug { - query = query.filter(projects_dsl::slug.eq(slug)); - } + let build_query = || { + let mut query = projects_dsl::projects.into_boxed(); - if let Some(raw_categories) = params.categories { - let categories: Vec = utils::parse_comma_values(&raw_categories); - query = query.filter(projects_dsl::categories.overlaps_with(categories)); - } + if let Some(slug) = params.slug.as_ref() { + query = query.filter(projects_dsl::slug.eq(slug)); + } - if let Some(raw_purposes) = params.purposes { - let purposes: Vec = utils::parse_comma_values(&raw_purposes); - query = query.filter(projects_dsl::purposes.overlaps_with(purposes)); - } + if let Some(raw_categories) = params.categories.as_ref() { + let categories: Vec = utils::parse_comma_values(raw_categories); + query = query.filter(projects_dsl::categories.overlaps_with(categories)); + } - if let Some(raw_technologies) = params.technologies { - let technologies: Vec = utils::parse_comma_values(&raw_technologies); - query = query.filter(projects_dsl::technologies.overlaps_with(technologies)); - } + if let Some(raw_purposes) = params.purposes.as_ref() { + let purposes: Vec = utils::parse_comma_values(raw_purposes); + query = query.filter(projects_dsl::purposes.overlaps_with(purposes)); + } - query = query.offset(pagination.offset).limit(pagination.limit); + if let Some(raw_technologies) = params.technologies.as_ref() { + let technologies: Vec = utils::parse_comma_values(raw_technologies); + query = query.filter(projects_dsl::technologies.overlaps_with(technologies)); + } - let result = query.load::(conn)?; - Ok(result) + query + }; + + let total_count = build_query().count().get_result::(conn)?; + + let result = build_query() + .offset(pagination.offset) + .limit(pagination.limit) + .load::(conn)?; + + Ok((result, total_count)) } fn by_id(&self, id: i32) -> Result, DBError> { diff --git a/src/api/projects/handlers.rs b/src/api/projects/handlers.rs index e2706f0..ef99b8a 100644 --- a/src/api/projects/handlers.rs +++ b/src/api/projects/handlers.rs @@ -16,8 +16,7 @@ pub async fn all_handler( params: QueryParams, pagination: PaginationParams, ) -> Result { - let projects = db_access.all(params, pagination.clone())?; - let total_count = projects.len() as i64; + let (projects, total_count) = db_access.all(params, pagination.clone())?; let has_next_page = pagination.offset + pagination.limit < total_count; let has_previous_page = pagination.offset > 0; diff --git a/src/utils.rs b/src/utils.rs index c03aa6d..8269d01 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,7 +2,7 @@ use ::warp::Reply; use warp::{filters::BoxedFilter, Filter}; use crate::{ - api::{health, projects, repositories}, + api::{health, issues, projects, repositories}, db::{ self, errors::DBError, @@ -43,12 +43,12 @@ pub fn setup_filters(db: DBAccess) -> BoxedFilter<(impl Reply,)> { let health_route = health::routes::routes(db.clone()); let projects_route = projects::routes::routes(db.clone()); let repositories_route = repositories::routes::routes(db.clone()); - // let issues_route = issues::routes::routes(db.clone()); + let issues_route = issues::routes::routes(db.clone()); health_route .or(projects_route) .or(repositories_route) - // .or(issues_route) + .or(issues_route) .with(warp::cors().allow_any_origin()) .recover(error_handler) .boxed() From db4c1f4c0b304780b346df133cdcda9c23e32efd Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sat, 10 Aug 2024 19:05:03 -0300 Subject: [PATCH 58/98] chore: update task name --- .github/workflows/prod.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 0146a40..6e715dc 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -24,7 +24,7 @@ permissions: contents: read jobs: - build: + production-deploy: runs-on: ubuntu-latest steps: @@ -74,4 +74,4 @@ jobs: task-definition: ${{ steps.task-def.outputs.task-definition }} service: ${{ env.ECS_SERVICE }} cluster: ${{ env.ECS_CLUSTER }} - # wait-for-service-stability: true \ No newline at end of file + wait-for-service-stability: true \ No newline at end of file From 21241137bb89ab3d88eb0f16d948c25aea01bd21 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sat, 10 Aug 2024 19:21:02 -0300 Subject: [PATCH 59/98] chore: test --- .github/workflows/prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 6e715dc..a95b3c1 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -74,4 +74,4 @@ jobs: task-definition: ${{ steps.task-def.outputs.task-definition }} service: ${{ env.ECS_SERVICE }} cluster: ${{ env.ECS_CLUSTER }} - wait-for-service-stability: true \ No newline at end of file + # wait-for-service-stability: true \ No newline at end of file From db9c3057ec021719f89c0e79ed6930ffad5219a5 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sat, 10 Aug 2024 20:48:17 -0300 Subject: [PATCH 60/98] chore: add curl --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 610ad7a..b5e0947 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,7 @@ ARG UID=10001 RUN apt-get update \ && apt-get install -y --no-install-recommends \ libpq5 \ + curl \ && rm -rf /var/lib/apt/lists/* RUN adduser \ From ce32f74922008a0571d20dfd61fc9f843014322f Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sat, 10 Aug 2024 23:16:58 -0300 Subject: [PATCH 61/98] chore: wait new deployment --- .github/workflows/prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index a95b3c1..6e715dc 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -74,4 +74,4 @@ jobs: task-definition: ${{ steps.task-def.outputs.task-definition }} service: ${{ env.ECS_SERVICE }} cluster: ${{ env.ECS_CLUSTER }} - # wait-for-service-stability: true \ No newline at end of file + wait-for-service-stability: true \ No newline at end of file From 41329f8fc705d46233b29b0ac0f82653ac271508 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 11 Aug 2024 16:06:31 -0300 Subject: [PATCH 62/98] chore: add dockerhub --- .github/workflows/prod.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 6e715dc..6a783bd 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -74,4 +74,15 @@ jobs: task-definition: ${{ steps.task-def.outputs.task-definition }} service: ${{ env.ECS_SERVICE }} cluster: ${{ env.ECS_CLUSTER }} - wait-for-service-stability: true \ No newline at end of file + wait-for-service-stability: true + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Tag and push docker image to Docker Hub + run: | + docker image tag ${{ steps.build-image.outputs.image }} $${{ vars.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} + docker push $image From b7fd8c95db21ea080e15e6bc14e4a835f8592598 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 11 Aug 2024 16:23:02 -0300 Subject: [PATCH 63/98] chore: add dockerhub --- .github/workflows/prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 6a783bd..a8c4ddb 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -84,5 +84,5 @@ jobs: - name: Tag and push docker image to Docker Hub run: | - docker image tag ${{ steps.build-image.outputs.image }} $${{ vars.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} + docker image tag ${{ steps.build-image.outputs.image }} ${{ vars.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} docker push $image From f91e8a0bf2167b0cbe9080bd9bfbd02fabab0aef Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 11 Aug 2024 16:30:32 -0300 Subject: [PATCH 64/98] chore: fix push dockerhub --- .github/workflows/prod.yml | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index a8c4ddb..dd777be 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -56,25 +56,25 @@ jobs: docker push $image echo "image=$image" >> $GITHUB_OUTPUT - - name: Download task definition - run: | - aws ecs describe-task-definition --task-definition issues-api --query taskDefinition > ${{ env.TASK_FILE }} + # - name: Download task definition + # run: | + # aws ecs describe-task-definition --task-definition issues-api --query taskDefinition > ${{ env.TASK_FILE }} - - name: Fill in the new image ID in the Amazon ECS task definition - id: task-def - uses: aws-actions/amazon-ecs-render-task-definition@v1 - with: - task-definition: ${{ env.TASK_FILE }} - container-name: ${{ env.ECS_CONTAINER }} - image: ${{ steps.build-image.outputs.image }} + # - name: Fill in the new image ID in the Amazon ECS task definition + # id: task-def + # uses: aws-actions/amazon-ecs-render-task-definition@v1 + # with: + # task-definition: ${{ env.TASK_FILE }} + # container-name: ${{ env.ECS_CONTAINER }} + # image: ${{ steps.build-image.outputs.image }} - - name: Deploy Amazon ECS task definition - uses: aws-actions/amazon-ecs-deploy-task-definition@v2 - with: - task-definition: ${{ steps.task-def.outputs.task-definition }} - service: ${{ env.ECS_SERVICE }} - cluster: ${{ env.ECS_CLUSTER }} - wait-for-service-stability: true + # - name: Deploy Amazon ECS task definition + # uses: aws-actions/amazon-ecs-deploy-task-definition@v2 + # with: + # task-definition: ${{ steps.task-def.outputs.task-definition }} + # service: ${{ env.ECS_SERVICE }} + # cluster: ${{ env.ECS_CLUSTER }} + # wait-for-service-stability: true - name: Log in to Docker Hub uses: docker/login-action@v3 @@ -84,5 +84,6 @@ jobs: - name: Tag and push docker image to Docker Hub run: | - docker image tag ${{ steps.build-image.outputs.image }} ${{ vars.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} + image=${{ vars.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} + docker image tag ${{ steps.build-image.outputs.image }} $image docker push $image From e57a6d334f96759a5b51fcd9f806d1761061bcdd Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 11 Aug 2024 16:36:18 -0300 Subject: [PATCH 65/98] chore: fix push dockerhub --- .github/workflows/prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index dd777be..1786a32 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -85,5 +85,5 @@ jobs: - name: Tag and push docker image to Docker Hub run: | image=${{ vars.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} - docker image tag ${{ steps.build-image.outputs.image }} $image + docker build -t $image . docker push $image From 559fe463516fd77093e07060c07b34aaf1ce4ec9 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 11 Aug 2024 16:44:29 -0300 Subject: [PATCH 66/98] fix: dockerhub --- .github/workflows/prod.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 1786a32..b9fdf0e 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -18,6 +18,7 @@ env: ECS_SERVICE: issues-api ECS_CONTAINER: issues-api TASK_FILE: task.json + DOCKERHUB_REGISTRY: kudosportal/issues permissions: id-token: write @@ -84,6 +85,7 @@ jobs: - name: Tag and push docker image to Docker Hub run: | - image=${{ vars.DOCKER_REGISTRY }}:${{ github.event.release.tag_name }} + image=${{ env.DOCKERHUB_REGISTRY }}:${{ github.event.release.tag_name }} + image=${{ env.DOCKERHUB_REGISTRY }}:${{ github.sha }} # TODO: delete docker build -t $image . docker push $image From 28c8bf2d103b8c2f2636f7c970f04e7f6bae15f8 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 11 Aug 2024 16:56:33 -0300 Subject: [PATCH 67/98] fix: dockerhub --- .github/workflows/prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index b9fdf0e..9389fce 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -86,6 +86,6 @@ jobs: - name: Tag and push docker image to Docker Hub run: | image=${{ env.DOCKERHUB_REGISTRY }}:${{ github.event.release.tag_name }} - image=${{ env.DOCKERHUB_REGISTRY }}:${{ github.sha }} # TODO: delete + image=${{ env.DOCKERHUB_REGISTRY }}:latest # TODO: delete docker build -t $image . docker push $image From 10370230c6b67bd9c6caaa674a67545616c5ff39 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 11 Aug 2024 17:04:03 -0300 Subject: [PATCH 68/98] chore: deplyo to stage --- .github/workflows/prod.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 9389fce..ae80976 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -84,8 +84,14 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Tag and push docker image to Docker Hub + id: build-dockerhub-image run: | image=${{ env.DOCKERHUB_REGISTRY }}:${{ github.event.release.tag_name }} image=${{ env.DOCKERHUB_REGISTRY }}:latest # TODO: delete docker build -t $image . docker push $image + echo "image=$image" >> $GITHUB_OUTPUT + + - name: Deploy staging + run: | + curl -f ${{ secrets.STAGE_DEPLOY_HOOK }}?imgURL=${{ steps.build-dockerhub-image.outputs.image }} From 3b260cf269dc7ba923ab52e53c65a74f39cc13a6 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 11 Aug 2024 17:09:39 -0300 Subject: [PATCH 69/98] chore: deplyo to stage --- .github/workflows/prod.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index ae80976..aa04e4e 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -94,4 +94,6 @@ jobs: - name: Deploy staging run: | - curl -f ${{ secrets.STAGE_DEPLOY_HOOK }}?imgURL=${{ steps.build-dockerhub-image.outputs.image }} + image=${{ steps.build-dockerhub-image.outputs.image }} + encoded_image=$(echo $image | jq -sRr @uri) + curl -f ${{ secrets.STAGE_DEPLOY_HOOK }}?imgURL=$encoded_image From 172222bff5edee57fd32456ead8def13f7bbd42e Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 11 Aug 2024 17:15:24 -0300 Subject: [PATCH 70/98] chore: deploy to stage --- .github/workflows/prod.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index aa04e4e..2d8e1a7 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -95,5 +95,5 @@ jobs: - name: Deploy staging run: | image=${{ steps.build-dockerhub-image.outputs.image }} - encoded_image=$(echo $image | jq -sRr @uri) - curl -f ${{ secrets.STAGE_DEPLOY_HOOK }}?imgURL=$encoded_image + hook=${{ secrets.STAGE_DEPLOY_HOOK }}?imgURL=$(echo $image | jq -sRr @uri) + curl -f $hook From e4fc6f2bbfc68ca9ee949d3fac7e104b24df7a16 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 11 Aug 2024 17:34:56 -0300 Subject: [PATCH 71/98] chore: deploy to stage --- .github/workflows/prod.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 2d8e1a7..627ff7b 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -94,6 +94,6 @@ jobs: - name: Deploy staging run: | - image=${{ steps.build-dockerhub-image.outputs.image }} - hook=${{ secrets.STAGE_DEPLOY_HOOK }}?imgURL=$(echo $image | jq -sRr @uri) + image=docker.io/${{ steps.build-dockerhub-image.outputs.image }} + hook=${{ secrets.STAGE_DEPLOY_HOOK }}&imgURL=$(echo $image | jq -sRr @uri) curl -f $hook From 3eafbb625ad5703b342a238fe1aec82f56026403 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 11 Aug 2024 17:39:51 -0300 Subject: [PATCH 72/98] chore: deploy to stage --- .github/workflows/prod.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 627ff7b..1356e54 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -94,6 +94,6 @@ jobs: - name: Deploy staging run: | - image=docker.io/${{ steps.build-dockerhub-image.outputs.image }} - hook=${{ secrets.STAGE_DEPLOY_HOOK }}&imgURL=$(echo $image | jq -sRr @uri) - curl -f $hook + image="docker.io/${{ steps.build-dockerhub-image.outputs.image }}" + encoded_image=$(echo $image | jq -sRr @uri) + curl -f "${{ secrets.STAGE_DEPLOY_HOOK }}&imgURL=$encoded_image" \ No newline at end of file From c471bebaa8e7a95f2f32202e7d03be6848a48c62 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 11 Aug 2024 17:58:49 -0300 Subject: [PATCH 73/98] chore: fix new line --- .github/workflows/prod.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 1356e54..6a83225 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -94,6 +94,6 @@ jobs: - name: Deploy staging run: | - image="docker.io/${{ steps.build-dockerhub-image.outputs.image }}" - encoded_image=$(echo $image | jq -sRr @uri) + image=docker.io/${{ steps.build-dockerhub-image.outputs.image }} + encoded_image=$(echo -n $image | jq -sRr @uri) curl -f "${{ secrets.STAGE_DEPLOY_HOOK }}&imgURL=$encoded_image" \ No newline at end of file From 751fdf4e9c6136e7f35ae519f8b2aa16f1e4c253 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 11 Aug 2024 18:15:17 -0300 Subject: [PATCH 74/98] chore: improve workflows --- .github/workflows/prod.yml | 51 +++++++++++++++---------------------- .github/workflows/stage.yml | 25 +++++++++++++++--- README.md | 4 +++ 3 files changed, 47 insertions(+), 33 deletions(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 6a83225..e41408e 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -1,15 +1,8 @@ name: Production -# TODO: restore -# on: -# release: -# types: [created] - -# TODO: delete -on: - push: - branches: - - chore/ecs +on: + release: + types: [created] env: AWS_REGION: us-east-1 @@ -52,30 +45,29 @@ jobs: REPOSITORY: issues-api run: | image=$REGISTRY/${{ env.ECR_REPOSITORY }}:${{ github.event.release.tag_name }} - image=$REGISTRY/${{ env.ECR_REPOSITORY }}:${{ github.sha }} # TODO: delete docker build -t $image . docker push $image echo "image=$image" >> $GITHUB_OUTPUT - # - name: Download task definition - # run: | - # aws ecs describe-task-definition --task-definition issues-api --query taskDefinition > ${{ env.TASK_FILE }} + - name: Download task definition + run: | + aws ecs describe-task-definition --task-definition issues-api --query taskDefinition > ${{ env.TASK_FILE }} - # - name: Fill in the new image ID in the Amazon ECS task definition - # id: task-def - # uses: aws-actions/amazon-ecs-render-task-definition@v1 - # with: - # task-definition: ${{ env.TASK_FILE }} - # container-name: ${{ env.ECS_CONTAINER }} - # image: ${{ steps.build-image.outputs.image }} + - name: Fill in the new image ID in the Amazon ECS task definition + id: task-def + uses: aws-actions/amazon-ecs-render-task-definition@v1 + with: + task-definition: ${{ env.TASK_FILE }} + container-name: ${{ env.ECS_CONTAINER }} + image: ${{ steps.build-image.outputs.image }} - # - name: Deploy Amazon ECS task definition - # uses: aws-actions/amazon-ecs-deploy-task-definition@v2 - # with: - # task-definition: ${{ steps.task-def.outputs.task-definition }} - # service: ${{ env.ECS_SERVICE }} - # cluster: ${{ env.ECS_CLUSTER }} - # wait-for-service-stability: true + - name: Deploy Amazon ECS task definition + uses: aws-actions/amazon-ecs-deploy-task-definition@v2 + with: + task-definition: ${{ steps.task-def.outputs.task-definition }} + service: ${{ env.ECS_SERVICE }} + cluster: ${{ env.ECS_CLUSTER }} + wait-for-service-stability: true - name: Log in to Docker Hub uses: docker/login-action@v3 @@ -83,11 +75,10 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Tag and push docker image to Docker Hub + - name: Build, tag and push docker image to Docker Hub id: build-dockerhub-image run: | image=${{ env.DOCKERHUB_REGISTRY }}:${{ github.event.release.tag_name }} - image=${{ env.DOCKERHUB_REGISTRY }}:latest # TODO: delete docker build -t $image . docker push $image echo "image=$image" >> $GITHUB_OUTPUT diff --git a/.github/workflows/stage.yml b/.github/workflows/stage.yml index 94d7803..564112e 100644 --- a/.github/workflows/stage.yml +++ b/.github/workflows/stage.yml @@ -5,8 +5,11 @@ on: branches: - main +env: + DOCKERHUB_REGISTRY: kudosportal/issues + jobs: - build: + stage-deploy: runs-on: ubuntu-latest steps: @@ -44,6 +47,22 @@ jobs: env: DATABASE_URL: ${{ secrets.TESTS_DATABASE_URL }} - - name: Build docker image + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build, tag and push docker image to Docker Hub + id: build-dockerhub-image + run: | + image=${{ env.DOCKERHUB_REGISTRY }}:${{ github.sha }} + docker build -t $image . + docker push $image + echo "image=$image" >> $GITHUB_OUTPUT + + - name: Deploy staging run: | - docker build -t ${{ vars.DOCKER_REGISTRY }}:${{ github.sha }} -f Dockerfile . + image=docker.io/${{ steps.build-dockerhub-image.outputs.image }} + encoded_image=$(echo -n $image | jq -sRr @uri) + curl -f "${{ secrets.STAGE_DEPLOY_HOOK }}&imgURL=$encoded_image" \ No newline at end of file diff --git a/README.md b/README.md index b1399c7..898d6a7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +[![Production](https://github.com/kudos-ink/issues-api/actions/workflows/prod.yml/badge.svg)](https://github.com/kudos-ink/issues-api/actions/workflows/prod.yml) +[![Stage](https://github.com/kudos-ink/issues-api/actions/workflows/stage.yml/badge.svg)](https://github.com/kudos-ink/issues-api/actions/workflows/stage.yml) +[![Stage cron](https://github.com/kudos-ink/issues-api/actions/workflows/stage_cron.yaml/badge.svg)](https://github.com/kudos-ink/issues-api/actions/workflows/stage_cron.yaml) + # kudos api # Local Development From 8ffe95ffc130595600706fe1dab99b1c23e67769 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 11 Aug 2024 18:22:36 -0300 Subject: [PATCH 75/98] chore: remove integration tests --- .github/workflows/stage.yml | 10 +++++----- .github/workflows/tests.yml | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/stage.yml b/.github/workflows/stage.yml index 564112e..22b5082 100644 --- a/.github/workflows/stage.yml +++ b/.github/workflows/stage.yml @@ -41,11 +41,11 @@ jobs: run: | make test - - name: Integration tests - run: | - make test-db - env: - DATABASE_URL: ${{ secrets.TESTS_DATABASE_URL }} + # - name: Integration tests + # run: | + # make test-db + # env: + # DATABASE_URL: ${{ secrets.TESTS_DATABASE_URL }} - name: Log in to Docker Hub uses: docker/login-action@v3 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b49e52d..9fe0608 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -39,11 +39,11 @@ jobs: run: | make test - - name: Integration tests - run: | - make test-db - env: - DATABASE_URL: ${{ secrets.TESTS_DATABASE_URL }} + # - name: Integration tests + # run: | + # make test-db + # env: + # DATABASE_URL: ${{ secrets.TESTS_DATABASE_URL }} - name: Build docker image run: | From 59abdb0cb5d7dac9fb7541fedcaf8ff51637232d Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 25 Aug 2024 20:21:57 -0300 Subject: [PATCH 76/98] feat: add users endpoints --- src/utils.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils.rs b/src/utils.rs index 8269d01..1999f13 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,7 +2,7 @@ use ::warp::Reply; use warp::{filters::BoxedFilter, Filter}; use crate::{ - api::{health, issues, projects, repositories}, + api::{health, issues, projects, repositories, users}, db::{ self, errors::DBError, @@ -44,11 +44,13 @@ pub fn setup_filters(db: DBAccess) -> BoxedFilter<(impl Reply,)> { let projects_route = projects::routes::routes(db.clone()); let repositories_route = repositories::routes::routes(db.clone()); let issues_route = issues::routes::routes(db.clone()); + let users_route = users::routes::routes(db.clone()); health_route .or(projects_route) .or(repositories_route) .or(issues_route) + .or(users_route) .with(warp::cors().allow_any_origin()) .recover(error_handler) .boxed() From 8d7e775e696e52b026c0f50c572f3fcb7f038530 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 25 Aug 2024 20:25:14 -0300 Subject: [PATCH 77/98] fix: minor fixes --- src/api/health/routes.rs | 6 ++---- src/api/issues/db.rs | 2 +- src/api/repositories/db.rs | 2 +- src/errors.rs | 4 ++-- src/utils.rs | 4 ++-- 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/api/health/routes.rs b/src/api/health/routes.rs index dfe8b6a..d3e14e6 100644 --- a/src/api/health/routes.rs +++ b/src/api/health/routes.rs @@ -5,10 +5,8 @@ use super::db::DBHealth; use super::handlers; pub fn routes(db_access: impl DBHealth) -> BoxedFilter<(impl Reply,)> { - let health_route = warp::path!("health") + warp::path!("health") .and(warp::any().map(move || db_access.clone())) .and_then(handlers::health_handler) - .boxed(); - - health_route + .boxed() } diff --git a/src/api/issues/db.rs b/src/api/issues/db.rs index a7ac76d..65ef8b1 100644 --- a/src/api/issues/db.rs +++ b/src/api/issues/db.rs @@ -74,7 +74,7 @@ impl DBIssue for DBAccess { } if let Some(raw_labels) = params.labels.as_ref() { - let labels: Vec = utils::parse_comma_values(&raw_labels); + let labels: Vec = utils::parse_comma_values(raw_labels); query = query.filter(issues_dsl::labels.overlaps_with(labels)); } diff --git a/src/api/repositories/db.rs b/src/api/repositories/db.rs index 1f41b6b..41c55e2 100644 --- a/src/api/repositories/db.rs +++ b/src/api/repositories/db.rs @@ -42,7 +42,7 @@ impl DBRepository for DBAccess { } if let Some(project_id) = params.project_ids { let ids: Vec = utils::parse_ids(&project_id); - if ids.len() > 0 { + if !ids.is_empty() { query = query.filter(repositories_dsl::project_id.eq_any(ids)); } } diff --git a/src/errors.rs b/src/errors.rs index c46db16..7eee0e0 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -34,10 +34,10 @@ pub async fn error_handler(err: Rejection) -> std::result::Result() { - eprintln!("AuthenticationError: {}", e.to_string()); + eprintln!("AuthenticationError: {e}"); ( StatusCode::UNAUTHORIZED, - format!("AuthenticationError - {}", e.to_string()), + format!("AuthenticationError - {e}"), ) } else if let Some(db_error) = err.find::() { match db_error { diff --git a/src/utils.rs b/src/utils.rs index 1999f13..de342a0 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -11,8 +11,8 @@ use crate::{ errors::error_handler, }; -pub async fn setup_db(url: &String) -> DBAccess { - let db_pool = db::pool::create_db_pool(&url) +pub async fn setup_db(url: &str) -> DBAccess { + let db_pool = db::pool::create_db_pool(url) .map_err(DBError::DBPoolConnection) .expect("Failed to create DB pool"); From 247af5a604419a009c09581fef61950cc963e5fb Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Wed, 28 Aug 2024 21:39:29 -0300 Subject: [PATCH 78/98] feat: improve issue creation --- Cargo.lock | 351 +++++++++++++++++++++++++++++++++---- Cargo.toml | 5 + src/api/issues/errors.rs | 18 +- src/api/issues/handlers.rs | 55 ++++-- src/api/issues/routes.rs | 2 +- src/errors.rs | 15 +- src/main.rs | 7 +- 7 files changed, 394 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 358b678..ebd7064 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -109,9 +158,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cc" @@ -140,9 +189,15 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets", + "windows-targets 0.48.5", ] +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -168,6 +223,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.76", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.76", +] + [[package]] name = "data-encoding" version = "2.5.0" @@ -198,7 +288,7 @@ dependencies = [ "diesel_table_macro_syntax", "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -207,7 +297,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" dependencies = [ - "syn", + "syn 2.0.76", ] [[package]] @@ -235,6 +325,29 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -414,6 +527,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.27" @@ -461,6 +580,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -481,6 +606,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.9" @@ -501,16 +632,21 @@ name = "kudos_api" version = "0.1.0" dependencies = [ "base64 0.22.0", + "bytes", "chrono", "diesel", "dotenv", + "env_logger", + "log", "regex", "serde", "serde_derive", "serde_json", + "serde_path_to_error", "thiserror", "tokio", "url", + "validator_derive", "warp", ] @@ -532,9 +668,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" @@ -575,7 +711,7 @@ checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -650,7 +786,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -676,7 +812,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -706,20 +842,44 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -847,22 +1007,22 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -876,6 +1036,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -931,7 +1101,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -940,11 +1110,27 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" -version = "2.0.39" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" dependencies = [ "proc-macro2", "quote", @@ -968,7 +1154,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -1000,7 +1186,7 @@ dependencies = [ "pin-project-lite", "socket2 0.5.5", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1011,7 +1197,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -1155,6 +1341,26 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "validator_derive" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55591299b7007f551ed1eb79a684af7672c19c3193fb9e0a31936987bb2438ec" +dependencies = [ + "darling", + "once_cell", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.76", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -1234,7 +1440,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.76", "wasm-bindgen-shared", ] @@ -1256,7 +1462,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1295,7 +1501,7 @@ version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -1304,7 +1510,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -1313,13 +1528,29 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -1328,38 +1559,86 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index 37f8a40..ddc71ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,8 @@ base64 = "0.22.0" url = "2.5.0" diesel = { version = "2.1.5", features = ["postgres", "chrono", "r2d2"] } regex = "1.10.4" +log = "0.4.22" +env_logger = "0.11.5" +bytes = "1.7.1" +serde_path_to_error = "0.1.16" +validator_derive = "0.18.1" diff --git a/src/api/issues/errors.rs b/src/api/issues/errors.rs index ca8a140..13fcd53 100644 --- a/src/api/issues/errors.rs +++ b/src/api/issues/errors.rs @@ -14,7 +14,9 @@ use crate::errors::ErrorResponse; pub enum IssueError { AlreadyExists(i32), NotFound(i32), - InvalidPayload, + RepositoryNotFound(i32), + InvalidPayload(String), + CannotCreate(String), } impl fmt::Display for IssueError { @@ -26,9 +28,15 @@ impl fmt::Display for IssueError { IssueError::NotFound(id) => { write!(f, "Issue #{id} not found") } - IssueError::InvalidPayload => { - write!(f, "Invalid payload") + IssueError::InvalidPayload(error) => { + write!(f, "Invalid payload: {error}") } + IssueError::CannotCreate(err) => { + write!(f, "error creating the issue: {err}") + }, + IssueError::RepositoryNotFound(id) => { + write!(f, "Repository #{id} does not exist") + }, } } } @@ -40,7 +48,9 @@ impl Reply for IssueError { let code = match self { IssueError::AlreadyExists(_) => StatusCode::BAD_REQUEST, IssueError::NotFound(_) => StatusCode::NOT_FOUND, - IssueError::InvalidPayload => StatusCode::UNPROCESSABLE_ENTITY, + IssueError::InvalidPayload(_) => StatusCode::UNPROCESSABLE_ENTITY, + IssueError::CannotCreate(_) => StatusCode::INTERNAL_SERVER_ERROR, + IssueError::RepositoryNotFound(_) => StatusCode::UNPROCESSABLE_ENTITY, }; let message = self.to_string(); diff --git a/src/api/issues/handlers.rs b/src/api/issues/handlers.rs index bc58ce9..672c8fb 100644 --- a/src/api/issues/handlers.rs +++ b/src/api/issues/handlers.rs @@ -1,5 +1,9 @@ +use bytes::Buf; +use log::{error, info, warn}; +use thiserror::Error; use warp::{ http::StatusCode, + reject, reject::Rejection, reply::{json, with_status, Reply}, }; @@ -27,6 +31,7 @@ pub async fn all_handler( params: QueryParams, pagination: PaginationParams, ) -> Result { + info!("getting all the issues"); let (issues, total_count) = db_access.all(params, pagination.clone())?; let has_next_page = pagination.offset + pagination.limit < total_count; let has_previous_page = pagination.offset > 0; @@ -42,19 +47,47 @@ pub async fn all_handler( } pub async fn create_handler( - issue: NewIssue, + buf: impl Buf, db_access: impl DBIssue + DBRepository, ) -> Result { + let des = &mut serde_json::Deserializer::from_reader(buf.reader()); + let issue: NewIssue = serde_path_to_error::deserialize(des).map_err(|e| { + let e = e.to_string(); + warn!("invalid issue '{e}'",); + reject::custom(IssueError::InvalidPayload(e)) + })?; + + info!("creating issue id '{}'", issue.id); match DBRepository::by_id(&db_access, issue.repository_id) { - Ok(_) => match db_access.by_number(issue.repository_id, issue.number)? { - Some(r) => Err(warp::reject::custom(IssueError::AlreadyExists(r.id))), - None => Ok(with_status( - // check if repository exists - json(&DBIssue::create(&db_access, &issue)?), - StatusCode::CREATED, - )), + Ok(repo) => match repo { + Some(_) => match db_access.by_number(issue.repository_id, issue.number)? { + Some(r) => { + warn!("issue id '{}' exists", issue.id); + Err(warp::reject::custom(IssueError::AlreadyExists(r.id))) + } + None => match DBIssue::create(&db_access, &issue) { + Ok(issue) => { + info!("issue id '{}' created", issue.id); + Ok(with_status(json(&issue), StatusCode::CREATED)) + } + Err(err) => { + error!("error creating the issue '{:?}': {}", issue, err); + Err(warp::reject::custom(IssueError::CannotCreate( + "error creating the issue".to_owned(), + ))) + } + }, + }, + None => { + warn!("repository '{}' invalid", issue.repository_id); + Err(warp::reject::custom(IssueError::RepositoryNotFound( + issue.repository_id + ))) + } }, - Err(_) => Err(warp::reject::custom(IssueError::InvalidPayload)), + Err(_) => Err(warp::reject::custom(IssueError::CannotCreate( + "cannot check if the repository is valid".to_owned(), + ))), } } pub async fn update_handler( @@ -64,7 +97,9 @@ pub async fn update_handler( ) -> Result { if let Some(repo_id) = issue.repository_id { if DBRepository::by_id(&db_access, repo_id).is_err() { - return Err(warp::reject::custom(IssueError::InvalidPayload)); + return Err(warp::reject::custom(IssueError::InvalidPayload( + "invalid".to_owned(), + ))); } } diff --git a/src/api/issues/routes.rs b/src/api/issues/routes.rs index 790cc79..fc479f6 100644 --- a/src/api/issues/routes.rs +++ b/src/api/issues/routes.rs @@ -36,7 +36,7 @@ pub fn routes(db_access: impl DBIssue + DBRepository) -> BoxedFilter<(impl Reply let create_issue = issue .and(with_auth()) .and(warp::post()) - .and(warp::body::json()) + .and(warp::body::aggregate()) .and(with_db(db_access.clone())) .and_then(handlers::create_handler); diff --git a/src/errors.rs b/src/errors.rs index 7eee0e0..9ceb35a 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -5,6 +5,7 @@ use warp::{hyper::StatusCode, Rejection, Reply}; use crate::{ auth::errors::AuthenticationError, + api::issues::errors::IssueError, db::errors::DBError, // pagination::{PaginationError, SortError}, // repository::{errors::RepositoryError, models::RepositorySortError}, @@ -17,13 +18,13 @@ pub struct ErrorResponse { } pub async fn error_handler(err: Rejection) -> std::result::Result { + if let Some(e) = err.find::() { + return Ok(e.clone().into_response()); + } + // TODO: add more errors + let (status, message) = if err.is_not_found() { (StatusCode::NOT_FOUND, "Resource not found".to_string()) - } else if err.find::().is_some() { - ( - StatusCode::METHOD_NOT_ALLOWED, - "Method not allowed".to_string(), - ) } else if let Some(e) = err.find::() { eprintln!("BodyDeserializeError error: {:?}", e); (StatusCode::BAD_REQUEST, "Invalid request body".to_string()) @@ -64,6 +65,6 @@ pub async fn error_handler(err: Rejection) -> std::result::Result() .expect("Invalid server address"); - println!("listening on {}", addr); + info!("listening on {}", addr); + warn!("listening on {}", addr); warp::serve(app_filters).run(addr).await; } From 388f49a03318212a368eacabc172a3c343c8b131 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Thu, 29 Aug 2024 09:01:21 -0300 Subject: [PATCH 79/98] feat: improve projects --- src/api/issues/errors.rs | 6 ++--- src/api/issues/handlers.rs | 2 +- src/api/projects/db.rs | 6 ++--- src/api/projects/errors.rs | 18 ++++++------- src/api/projects/handlers.rs | 36 +++++++++++++++++++------ src/api/projects/models.rs | 2 +- src/api/projects/routes.rs | 2 +- src/api/repositories/errors.rs | 20 +++++++++++--- src/api/repositories/handlers.rs | 45 +++++++++++++++++++++++++------- src/api/repositories/routes.rs | 9 ++++--- src/errors.rs | 8 +++++- 11 files changed, 111 insertions(+), 43 deletions(-) diff --git a/src/api/issues/errors.rs b/src/api/issues/errors.rs index 13fcd53..3fdc893 100644 --- a/src/api/issues/errors.rs +++ b/src/api/issues/errors.rs @@ -31,11 +31,11 @@ impl fmt::Display for IssueError { IssueError::InvalidPayload(error) => { write!(f, "Invalid payload: {error}") } - IssueError::CannotCreate(err) => { - write!(f, "error creating the issue: {err}") + IssueError::CannotCreate(error) => { + write!(f, "error creating the issue: {error}") }, IssueError::RepositoryNotFound(id) => { - write!(f, "Repository #{id} does not exist") + write!(f, "Repository '{id}' does not exist") }, } } diff --git a/src/api/issues/handlers.rs b/src/api/issues/handlers.rs index 672c8fb..c778296 100644 --- a/src/api/issues/handlers.rs +++ b/src/api/issues/handlers.rs @@ -1,6 +1,5 @@ use bytes::Buf; use log::{error, info, warn}; -use thiserror::Error; use warp::{ http::StatusCode, reject, @@ -20,6 +19,7 @@ use super::{ }; pub async fn by_id(id: i32, db_access: impl DBIssue) -> Result { + info!("getting issues '{id}'"); match db_access.by_id(id)? { None => Err(warp::reject::custom(IssueError::NotFound(id)))?, Some(repository) => Ok(json(&repository)), diff --git a/src/api/projects/db.rs b/src/api/projects/db.rs index 7242be3..4317694 100644 --- a/src/api/projects/db.rs +++ b/src/api/projects/db.rs @@ -1,7 +1,7 @@ use diesel::dsl::now; use diesel::prelude::*; -use super::models::{NewForm, Project, QueryParams, UpdateForm}; +use super::models::{NewProject, Project, QueryParams, UpdateForm}; use crate::schema::projects::dsl as projects_dsl; use crate::db::{ @@ -19,7 +19,7 @@ pub trait DBProject: Send + Sync + Clone + 'static { ) -> Result<(Vec, i64), DBError>; fn by_id(&self, id: i32) -> Result, DBError>; fn by_slug(&self, slug: &str) -> Result, DBError>; - fn create(&self, form: &NewForm) -> Result; + fn create(&self, form: &NewProject) -> Result; fn update(&self, id: i32, form: &UpdateForm) -> Result; fn delete(&self, id: i32) -> Result<(), DBError>; } @@ -91,7 +91,7 @@ impl DBProject for DBAccess { Ok(result) } - fn create(&self, form: &NewForm) -> Result { + fn create(&self, form: &NewProject) -> Result { let conn = &mut self.get_db_conn(); let project = diesel::insert_into(projects_dsl::projects) diff --git a/src/api/projects/errors.rs b/src/api/projects/errors.rs index 18c378b..904831f 100644 --- a/src/api/projects/errors.rs +++ b/src/api/projects/errors.rs @@ -15,20 +15,18 @@ pub enum ProjectError { AlreadyExists(i32), NotFound(i32), NotFoundBySlug(String), + InvalidPayload(String), + CannotCreate(String), } impl fmt::Display for ProjectError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ProjectError::AlreadyExists(id) => { - write!(f, "Project #{id} already exists") - } - ProjectError::NotFound(id) => { - write!(f, "Project #{id} not found") - } - ProjectError::NotFoundBySlug(slug) => { - write!(f, "Project {slug} not found") - } + ProjectError::AlreadyExists(id) => write!(f, "Project #{id} already exists"), + ProjectError::NotFound(id) => write!(f, "Project #{id} not found"), + ProjectError::NotFoundBySlug(slug) => write!(f, "Project {slug} not found"), + ProjectError::InvalidPayload(error) => write!(f, "Invalid payload: {error}"), + ProjectError::CannotCreate(error) => write!(f, "Cannot create the project: {error}"), } } } @@ -41,6 +39,8 @@ impl Reply for ProjectError { ProjectError::AlreadyExists(_) => StatusCode::BAD_REQUEST, ProjectError::NotFound(_) => StatusCode::NOT_FOUND, ProjectError::NotFoundBySlug(_) => StatusCode::NOT_FOUND, + ProjectError::InvalidPayload(_) => StatusCode::UNPROCESSABLE_ENTITY, + ProjectError::CannotCreate(_) => StatusCode::INTERNAL_SERVER_ERROR, }; let message = self.to_string(); diff --git a/src/api/projects/handlers.rs b/src/api/projects/handlers.rs index ef99b8a..95edb7c 100644 --- a/src/api/projects/handlers.rs +++ b/src/api/projects/handlers.rs @@ -3,13 +3,16 @@ use crate::types::{PaginatedResponse, PaginationParams}; use super::{ db::DBProject, errors::ProjectError, - models::{NewForm, QueryParams, UpdateForm}, + models::{NewProject, QueryParams, UpdateForm}, }; use warp::{ http::StatusCode, reject::Rejection, + reject, reply::{json, with_status, Reply}, }; +use log::{error, info, warn}; +use bytes::Buf; pub async fn all_handler( db_access: impl DBProject, @@ -31,16 +34,33 @@ pub async fn all_handler( } pub async fn create_handler( - form: NewForm, + buf: impl Buf, db_access: impl DBProject, ) -> Result { - match db_access.by_slug(&form.slug)? { + let des = &mut serde_json::Deserializer::from_reader(buf.reader()); + let project: NewProject = serde_path_to_error::deserialize(des).map_err(|e| { + let e = e.to_string(); + warn!("invalid project '{e}'",); + reject::custom(ProjectError::InvalidPayload(e)) + })?; + match db_access.by_slug(&project.slug)? { Some(p) => Err(warp::reject::custom(ProjectError::AlreadyExists(p.id))), - None => Ok(with_status( - json(&db_access.create(&form)?), - StatusCode::CREATED, - )), - } + None => match db_access.create(&project) { + Ok(project) => { + info!("project slug '{}' created", project.slug); + Ok(with_status( + json(&project), + StatusCode::CREATED, + )) + }, + Err(error) => { + error!("error creating the project '{:?}': {}", project, error); + Err(warp::reject::custom(ProjectError::CannotCreate( + "error creating the project".to_string(), + ))) + }, + }, + } } pub async fn update_handler( diff --git a/src/api/projects/models.rs b/src/api/projects/models.rs index 954fcfc..5401f68 100644 --- a/src/api/projects/models.rs +++ b/src/api/projects/models.rs @@ -32,7 +32,7 @@ pub struct QueryParams { #[derive(Insertable, Serialize, Deserialize, Debug)] #[diesel(table_name = projects)] -pub struct NewForm { +pub struct NewProject { pub name: String, pub slug: String, pub categories: Option>>, diff --git a/src/api/projects/routes.rs b/src/api/projects/routes.rs index 199e672..92c4a82 100644 --- a/src/api/projects/routes.rs +++ b/src/api/projects/routes.rs @@ -29,7 +29,7 @@ pub fn routes(db_access: impl DBProject) -> BoxedFilter<(impl Reply,)> { let create_route = project .and(with_auth()) .and(warp::post()) - .and(warp::body::json()) + .and(warp::body::aggregate()) .and(with_db(db_access.clone())) .and_then(handlers::create_handler); diff --git a/src/api/repositories/errors.rs b/src/api/repositories/errors.rs index 329966e..fd40dfb 100644 --- a/src/api/repositories/errors.rs +++ b/src/api/repositories/errors.rs @@ -15,20 +15,31 @@ pub enum RepositoryError { AlreadyExists(i32), NotFound(i32), NotFoundByName(String), + ProjectNotFound(i32), + InvalidPayload(String), + CannotCreate(String), } impl fmt::Display for RepositoryError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { RepositoryError::AlreadyExists(id) => { - write!(f, "Repository #{id} already exists") + write!(f, "Repository '{id}' already exists") } RepositoryError::NotFound(id) => { - write!(f, "Repository #{id} not found") + write!(f, "Repository '{id}' not found") } RepositoryError::NotFoundByName(name) => { - write!(f, "Repository {name} not found") + write!(f, "Repository '{name}' not found") } + RepositoryError::InvalidPayload(error) => { + write!(f, "Invalid payload: {error}") + }, + RepositoryError::CannotCreate(err) => { + write!(f, "error creating the repository: {err}") + }, + RepositoryError::ProjectNotFound(id) => { + write!(f, "Project id '{id}' does not exist")}, } } } @@ -41,6 +52,9 @@ impl Reply for RepositoryError { RepositoryError::AlreadyExists(_) => StatusCode::BAD_REQUEST, RepositoryError::NotFound(_) => StatusCode::NOT_FOUND, RepositoryError::NotFoundByName(_) => StatusCode::NOT_FOUND, + RepositoryError::InvalidPayload(_) => StatusCode::UNPROCESSABLE_ENTITY, + RepositoryError::CannotCreate(_) => StatusCode::INTERNAL_SERVER_ERROR, + RepositoryError::ProjectNotFound(_) => StatusCode::UNPROCESSABLE_ENTITY, }; let message = self.to_string(); diff --git a/src/api/repositories/handlers.rs b/src/api/repositories/handlers.rs index 6fe8523..293f8d4 100644 --- a/src/api/repositories/handlers.rs +++ b/src/api/repositories/handlers.rs @@ -1,10 +1,13 @@ use warp::{ http::StatusCode, reject::Rejection, + reject, reply::{json, with_status, Reply}, }; +use log::{error, info, warn}; +use bytes::Buf; -use crate::types::{PaginatedResponse, PaginationParams}; +use crate::{api::projects::db::DBProject, types::{PaginatedResponse, PaginationParams}}; use super::{ db::DBRepository, @@ -40,16 +43,40 @@ pub async fn all_handler( } pub async fn create_handler( - repo: NewRepository, - db_access: impl DBRepository, + buf: impl Buf, + db_access: impl DBRepository + DBProject, ) -> Result { - match db_access.by_slug(&repo.slug)? { + + let des = &mut serde_json::Deserializer::from_reader(buf.reader()); + let repository: NewRepository = serde_path_to_error::deserialize(des).map_err(|e| { + let e = e.to_string(); + warn!("invalid repository '{e}'",); + reject::custom(RepositoryError::InvalidPayload(e)) + })?; + match DBRepository::by_slug(&db_access, &repository.slug)? { Some(r) => Err(warp::reject::custom(RepositoryError::AlreadyExists(r.id))), - None => Ok(with_status( - // check if project exists - json(&db_access.create(&repo)?), - StatusCode::CREATED, - )), + None => match DBProject::by_id(&db_access, repository.project_id) { + Ok(project) => match project { + Some(_) => match DBRepository::create(&db_access, &repository){ + Ok(_) => { + info!("repository slug '{}' created", repository.slug); + Ok(with_status(json(&repository), StatusCode::CREATED))}, + Err(err) => { + error!("error creating the repository '{:?}': {}", repository, err); + Err(warp::reject::custom(RepositoryError::CannotCreate( + "error creating the repository".to_owned(), + ))) + }, + }, + None => { + warn!("project id '{}' does not exist", repository.project_id); + Err(warp::reject::custom(RepositoryError::ProjectNotFound(repository.project_id))) + }, + }, + Err(_) => Err(warp::reject::custom(RepositoryError::CannotCreate( + "cannot check if the repository exists".to_owned(), + ))), + } } } pub async fn update_handler( diff --git a/src/api/repositories/routes.rs b/src/api/repositories/routes.rs index bc7e709..f3896aa 100644 --- a/src/api/repositories/routes.rs +++ b/src/api/repositories/routes.rs @@ -3,6 +3,7 @@ use std::convert::Infallible; use warp::filters::BoxedFilter; use warp::{Filter, Reply}; +use crate::api::projects::db::DBProject; use crate::auth::with_auth; use crate::types::PaginationParams; @@ -13,12 +14,12 @@ use super::models::QueryParams; // use crate::pagination::GetSort; fn with_db( - db_pool: impl DBRepository, -) -> impl Filter + Clone { + db_pool: impl DBRepository + DBProject, +) -> impl Filter + Clone { warp::any().map(move || db_pool.clone()) } -pub fn routes(db_access: impl DBRepository) -> BoxedFilter<(impl Reply,)> { +pub fn routes(db_access: impl DBRepository + DBProject) -> BoxedFilter<(impl Reply,)> { let repository = warp::path!("repositories"); let repository_id = warp::path!("repositories" / i32); @@ -37,7 +38,7 @@ pub fn routes(db_access: impl DBRepository) -> BoxedFilter<(impl Reply,)> { let create_route = repository .and(with_auth()) .and(warp::post()) - .and(warp::body::json()) + .and(warp::body::aggregate()) .and(with_db(db_access.clone())) .and_then(handlers::create_handler); diff --git a/src/errors.rs b/src/errors.rs index 9ceb35a..60cc4a1 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -6,6 +6,8 @@ use warp::{hyper::StatusCode, Rejection, Reply}; use crate::{ auth::errors::AuthenticationError, api::issues::errors::IssueError, + api::repositories::errors::RepositoryError, + api::projects::errors::ProjectError, db::errors::DBError, // pagination::{PaginationError, SortError}, // repository::{errors::RepositoryError, models::RepositorySortError}, @@ -20,9 +22,13 @@ pub struct ErrorResponse { pub async fn error_handler(err: Rejection) -> std::result::Result { if let Some(e) = err.find::() { return Ok(e.clone().into_response()); + } else if let Some(e) = err.find::() { + return Ok(e.clone().into_response()); + }else if let Some(e) = err.find::() { + return Ok(e.clone().into_response()); } // TODO: add more errors - + let (status, message) = if err.is_not_found() { (StatusCode::NOT_FOUND, "Resource not found".to_string()) } else if let Some(e) = err.find::() { From 45b8a4b43139bb4a9a1cb7a28493513e8bfa19b4 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Thu, 29 Aug 2024 09:26:09 -0300 Subject: [PATCH 80/98] chore: use newer rust image --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b5e0947..fdaca83 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -ARG RUST_VERSION=1.74.0 +ARG RUST_VERSION=1.78.0 FROM rust:${RUST_VERSION} AS build RUN --mount=type=bind,source=src,target=src \ From 4f8afaa9d8b269829c449759b2218e9615dcaf0a Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Thu, 5 Sep 2024 08:29:03 -0300 Subject: [PATCH 81/98] chore: migrate schema --- Makefile | 4 ++++ migrations/2024-04-13-204151_init/down.sql | 5 +++-- migrations/2024-04-13-204151_init/up.sql | 8 +++++--- src/api/issues/models.rs | 4 +--- src/api/repositories/models.rs | 4 ++++ src/schema.rs | 11 +++++++---- 6 files changed, 24 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 0559cd9..582ad26 100644 --- a/Makefile +++ b/Makefile @@ -39,6 +39,10 @@ db-down: db-migrate: DATABASE_URL="$(DATABASE_URL)" diesel migration run +.PHONY: db-migrate-redo +db-migrate-redo: + DATABASE_URL="$(DATABASE_URL)" diesel migration redo + # Clean up the Docker volume .PHONY: db-clean db-clean: diff --git a/migrations/2024-04-13-204151_init/down.sql b/migrations/2024-04-13-204151_init/down.sql index 79b2107..411709a 100644 --- a/migrations/2024-04-13-204151_init/down.sql +++ b/migrations/2024-04-13-204151_init/down.sql @@ -1,5 +1,6 @@ -- Drop tables -DROP TABLE IF EXISTS projects; +DROP TABLE IF EXISTS issues; DROP TABLE IF EXISTS repositories; +DROP TABLE IF EXISTS projects; DROP TABLE IF EXISTS users; -DROP TABLE IF EXISTS issues; \ No newline at end of file +DROP TABLE IF EXISTS languages; \ No newline at end of file diff --git a/migrations/2024-04-13-204151_init/up.sql b/migrations/2024-04-13-204151_init/up.sql index e5bfbaa..c5e09f5 100644 --- a/migrations/2024-04-13-204151_init/up.sql +++ b/migrations/2024-04-13-204151_init/up.sql @@ -20,6 +20,8 @@ CREATE TABLE IF NOT EXISTS languages ( CREATE TABLE IF NOT EXISTS repositories ( id SERIAL PRIMARY KEY, slug VARCHAR(255) NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL, + url VARCHAR(255) NOT NULL, language_slug VARCHAR(255) NOT NULL, project_id INT REFERENCES projects(id) NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, @@ -28,7 +30,7 @@ CREATE TABLE IF NOT EXISTS repositories ( -- all the users including maintainers and admins CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, - username VARCHAR(100) UNIQUE NOT NULL, + username VARCHAR(255) UNIQUE NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NULL ); @@ -36,11 +38,11 @@ CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS issues ( id SERIAL PRIMARY KEY, number int NOT NULL, - title VARCHAR(100) NOT NULL, + title VARCHAR(255) NOT NULL, labels TEXT [], open boolean DEFAULT true NOT NULL, + certified boolean, assignee_id INT REFERENCES users(id) NULL, - e_tag VARCHAR(100) NOT NULL, repository_id INT REFERENCES repositories(id) NOT NULL, issue_created_at TIMESTAMP WITH TIME ZONE NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, diff --git a/src/api/issues/models.rs b/src/api/issues/models.rs index 7e65049..6ecaba8 100644 --- a/src/api/issues/models.rs +++ b/src/api/issues/models.rs @@ -15,8 +15,8 @@ pub struct Issue { pub title: String, pub labels: Option>>, pub open: bool, + pub certified: Option, pub assignee_id: Option, - pub e_tag: String, pub repository_id: i32, pub issue_created_at: DateTime, pub created_at: DateTime, @@ -33,7 +33,6 @@ pub struct NewIssue { pub open: bool, pub repository_id: i32, pub assignee_id: Option, - pub e_tag: String, pub issue_created_at: DateTime, } @@ -47,7 +46,6 @@ pub struct UpdateIssue { pub open: bool, pub repository_id: Option, pub assignee_id: Option, - pub e_tag: String, pub issue_created_at: DateTime, } diff --git a/src/api/repositories/models.rs b/src/api/repositories/models.rs index 25f6a92..7079c9d 100644 --- a/src/api/repositories/models.rs +++ b/src/api/repositories/models.rs @@ -12,6 +12,8 @@ use serde_derive::{Deserialize, Serialize}; pub struct Repository { pub id: i32, pub slug: String, + pub name: String, + pub url: String, pub language_slug: String, pub project_id: i32, pub created_at: DateTime, @@ -38,6 +40,8 @@ pub struct NewRepository { #[diesel(table_name = repositories)] pub struct UpdateRepository { pub slug: Option, + pub name: Option, + pub url: Option, pub language_slug: Option, pub project_id: Option, } diff --git a/src/schema.rs b/src/schema.rs index 5feab7e..ce84804 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -4,13 +4,12 @@ diesel::table! { issues (id) { id -> Int4, number -> Int4, - #[max_length = 100] + #[max_length = 255] title -> Varchar, labels -> Nullable>>, open -> Bool, + certified -> Nullable, assignee_id -> Nullable, - #[max_length = 100] - e_tag -> Varchar, repository_id -> Int4, issue_created_at -> Timestamptz, created_at -> Timestamptz, @@ -50,6 +49,10 @@ diesel::table! { #[max_length = 255] slug -> Varchar, #[max_length = 255] + name -> Varchar, + #[max_length = 255] + url -> Varchar, + #[max_length = 255] language_slug -> Varchar, project_id -> Int4, created_at -> Timestamptz, @@ -60,7 +63,7 @@ diesel::table! { diesel::table! { users (id) { id -> Int4, - #[max_length = 100] + #[max_length = 255] username -> Varchar, created_at -> Timestamptz, updated_at -> Nullable, From 92e54bb8aa3d84016c2018ec1e8f0f3e0dd6b047 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Thu, 5 Sep 2024 08:54:04 -0300 Subject: [PATCH 82/98] fix: models --- src/api/issues/models.rs | 3 ++- src/api/repositories/models.rs | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/api/issues/models.rs b/src/api/issues/models.rs index 6ecaba8..4f3896d 100644 --- a/src/api/issues/models.rs +++ b/src/api/issues/models.rs @@ -26,11 +26,11 @@ pub struct Issue { #[derive(Insertable, Serialize, Deserialize, Debug)] #[diesel(table_name = issues)] pub struct NewIssue { - pub id: i32, pub number: i32, pub title: String, pub labels: Option>, pub open: bool, + pub certified: Option, pub repository_id: i32, pub assignee_id: Option, pub issue_created_at: DateTime, @@ -44,6 +44,7 @@ pub struct UpdateIssue { pub title: String, pub labels: Option>, pub open: bool, + pub certified: Option, pub repository_id: Option, pub assignee_id: Option, pub issue_created_at: DateTime, diff --git a/src/api/repositories/models.rs b/src/api/repositories/models.rs index 7079c9d..9087fc5 100644 --- a/src/api/repositories/models.rs +++ b/src/api/repositories/models.rs @@ -32,6 +32,8 @@ pub struct QueryParams { #[diesel(table_name = repositories)] pub struct NewRepository { pub slug: String, + pub name: String, + pub url: String, pub language_slug: String, pub project_id: i32, } From 0d5321f6a89a8ea5762fa9c6b34b01ede276ca14 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Thu, 5 Sep 2024 08:54:16 -0300 Subject: [PATCH 83/98] fix: issue handler --- src/api/issues/handlers.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/issues/handlers.rs b/src/api/issues/handlers.rs index 99a9b7d..ceb7274 100644 --- a/src/api/issues/handlers.rs +++ b/src/api/issues/handlers.rs @@ -57,17 +57,17 @@ pub async fn create_handler( reject::custom(IssueError::InvalidPayload(e)) })?; - info!("creating issue id '{}'", issue.id); + info!("creating issue number '{}'", issue.number); match DBRepository::by_id(&db_access, issue.repository_id) { Ok(repo) => match repo { Some(_) => match db_access.by_number(issue.repository_id, issue.number)? { Some(r) => { - warn!("issue id '{}' exists", issue.id); - Err(warp::reject::custom(IssueError::AlreadyExists(r.id))) + warn!("issue number '{}' exists", issue.number); + Err(warp::reject::custom(IssueError::AlreadyExists(r.number))) } None => match DBIssue::create(&db_access, &issue) { Ok(issue) => { - info!("issue id '{}' created", issue.id); + info!("issue number '{}' created", issue.number); Ok(with_status(json(&issue), StatusCode::CREATED)) } Err(err) => { From dd5fda38dde59a728dc310b0d9cc63a99f84eeda Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Thu, 5 Sep 2024 08:54:33 -0300 Subject: [PATCH 84/98] chore: add script to generate test data --- mock_data.bash | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 mock_data.bash diff --git a/mock_data.bash b/mock_data.bash new file mode 100644 index 0000000..6d45d18 --- /dev/null +++ b/mock_data.bash @@ -0,0 +1,95 @@ +#!/bin/bash + +BASE_URL=${BASE_URL:-"http://localhost:8000"} +AUTH_HEADER="Authorization: Basic ${AUTH_TOKEN}" +CONTENT_TYPE_HEADER="Content-Type: application/json" + +# Create Polkadot project +curl --location "$BASE_URL/projects" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "name": "polkadot", + "slug":"polkadot" +}' + +# Create Asar project +curl --location "$BASE_URL/projects" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "name": "astar", + "slug":"astar" +}' +# Create Polkadot SDK repository +curl --location "$BASE_URL/repositories" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "name": "Polkadot SDK", + "slug": "polkadotsdk", + "language_slug": "rust", + "url": "https://github.com/paritytech/polkadot-sdk", + "project_id": 1 +}' + +# Create Zombienet repository +curl --location "$BASE_URL/repositories" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "name": "Zombienet", + "slug": "zombienet", + "language_slug": "rust", + "url": "https://github.com/paritytech/zombienet", + "project_id": 1 +}' + +# Create Astar repository +curl --location "$BASE_URL/repositories" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "name": "Astar", + "slug": "astar", + "language_slug": "rust", + "url": "https://github.com/AstarNetwork/Astar", + "project_id": 2 +}' + +# Create issues +curl --location "$BASE_URL/issues" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "number": 1863, + "title": "Adding separate label to the zombie namespaces", + "open": true, + "certified": false, + "repository_id": 4, + "issue_created_at": "2024-09-03T09:13:54Z" +}' + +curl --location "$BASE_URL/issues" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "number": 5597, + "title": "Fix balance to u256 type", + "open": true, + "certified": false, + "repository_id": 3, + "issue_created_at": "2024-09-03T09:13:54Z" +}' + +curl --location "$BASE_URL/repositories" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "number": 1350, + "title": "Update mocks to use TestDefaultConfig", + "open": true, + "certified": false, + "repository_id": 5, + "issue_created_at": "2024-09-03T09:13:54Z" +}' \ No newline at end of file From 1a618f8eabd5fdc9d599a8f54a4c5a747cad6f68 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Thu, 5 Sep 2024 09:05:21 -0300 Subject: [PATCH 85/98] chore: fix ids and endpoint in mock data script --- mock_data.bash | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mock_data.bash b/mock_data.bash index 6d45d18..53751e3 100644 --- a/mock_data.bash +++ b/mock_data.bash @@ -66,7 +66,7 @@ curl --location "$BASE_URL/issues" \ "title": "Adding separate label to the zombie namespaces", "open": true, "certified": false, - "repository_id": 4, + "repository_id": 1, "issue_created_at": "2024-09-03T09:13:54Z" }' @@ -78,11 +78,11 @@ curl --location "$BASE_URL/issues" \ "title": "Fix balance to u256 type", "open": true, "certified": false, - "repository_id": 3, + "repository_id": 2, "issue_created_at": "2024-09-03T09:13:54Z" }' -curl --location "$BASE_URL/repositories" \ +curl --location "$BASE_URL/issues" \ --header "$AUTH_HEADER" \ --header "$CONTENT_TYPE_HEADER" \ --data '{ @@ -90,6 +90,6 @@ curl --location "$BASE_URL/repositories" \ "title": "Update mocks to use TestDefaultConfig", "open": true, "certified": false, - "repository_id": 5, + "repository_id": 3, "issue_created_at": "2024-09-03T09:13:54Z" }' \ No newline at end of file From ed419175a0306c75909f49b693b3f61e540aa8b7 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 8 Sep 2024 13:39:38 -0300 Subject: [PATCH 86/98] chore: add delete on cascade --- Makefile | 10 +++++++--- migrations/2024-04-13-204151_init/up.sql | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 582ad26..0488c52 100644 --- a/Makefile +++ b/Makefile @@ -35,10 +35,14 @@ db-up: db-down: $(DOCKER_COMPOSE) -f $(DOCKER_COMPOSE_FILE) down $(DOCKER_DB_CONTAINER_NAME) -.PHONY: db-migrate -db-migrate: +.PHONY: db-migrate-up +db-migrate-up: DATABASE_URL="$(DATABASE_URL)" diesel migration run +.PHONY: db-migrate-down +db-migrate-down: + DATABASE_URL="$(DATABASE_URL)" diesel migration revert + .PHONY: db-migrate-redo db-migrate-redo: DATABASE_URL="$(DATABASE_URL)" diesel migration redo @@ -50,4 +54,4 @@ db-clean: .PHONY: test-db test-db: - DATABASE_URL="$(DATABASE_URL)" cargo test -- --ignored --test-threads=1 + DATABASE_URL="$(DATABASE_URL)" cargo test -- --ignored --test-threads=1 \ No newline at end of file diff --git a/migrations/2024-04-13-204151_init/up.sql b/migrations/2024-04-13-204151_init/up.sql index c5e09f5..29a4ed3 100644 --- a/migrations/2024-04-13-204151_init/up.sql +++ b/migrations/2024-04-13-204151_init/up.sql @@ -23,7 +23,7 @@ CREATE TABLE IF NOT EXISTS repositories ( name VARCHAR(255) NOT NULL, url VARCHAR(255) NOT NULL, language_slug VARCHAR(255) NOT NULL, - project_id INT REFERENCES projects(id) NOT NULL, + project_id INT REFERENCES projects(id) ON DELETE CASCADE NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NULL ); @@ -43,7 +43,7 @@ CREATE TABLE IF NOT EXISTS issues ( open boolean DEFAULT true NOT NULL, certified boolean, assignee_id INT REFERENCES users(id) NULL, - repository_id INT REFERENCES repositories(id) NOT NULL, + repository_id INT REFERENCES repositories(id) ON DELETE CASCADE NOT NULL, issue_created_at TIMESTAMP WITH TIME ZONE NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NULL From 5352ae6051002391be1bea7f9cc8cda297de5f0a Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 8 Sep 2024 16:36:29 -0300 Subject: [PATCH 87/98] feat: improve users endpoints --- src/api/users/errors.rs | 29 ++++++++++++----------------- src/api/users/handlers.rs | 37 ++++++++++++++++++++++++++++++------- src/api/users/routes.rs | 2 +- src/errors.rs | 17 +++++++---------- 4 files changed, 50 insertions(+), 35 deletions(-) diff --git a/src/api/users/errors.rs b/src/api/users/errors.rs index 2e32d83..f6c7e35 100644 --- a/src/api/users/errors.rs +++ b/src/api/users/errors.rs @@ -15,28 +15,22 @@ pub enum UserError { AlreadyExists(i32), NotFound(i32), NotFoundByName(String), - CannotBeCreated(String), - CannotBeUpdated(i32, String), + CannotCreate(String), + CannotUpdate(i32, String), + InvalidPayload(String), } impl fmt::Display for UserError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - UserError::AlreadyExists(id) => { - write!(f, "User #{id} already exists") - } - UserError::NotFound(id) => { - write!(f, "User #{id} not found") - } - UserError::NotFoundByName(name) => { - write!(f, "User {name} not found") - } - UserError::CannotBeCreated(error) => { - write!(f, "User cannot be created: {error}") - } - UserError::CannotBeUpdated(id, error) => { + UserError::AlreadyExists(id) => write!(f, "User #{id} already exists"), + UserError::NotFound(id) => write!(f, "User #{id} not found"), + UserError::NotFoundByName(name) => write!(f, "User {name} not found"), + UserError::CannotCreate(error) => write!(f, "User cannot be created: {error}"), + UserError::CannotUpdate(id, error) => { write!(f, "User #{id} cannot be updated: {error}") } + UserError::InvalidPayload(error) => write!(f, "Cannot create the user: {error}"), } } } @@ -49,8 +43,9 @@ impl Reply for UserError { UserError::AlreadyExists(_) => StatusCode::BAD_REQUEST, UserError::NotFound(_) => StatusCode::NOT_FOUND, UserError::NotFoundByName(_) => StatusCode::NOT_FOUND, - UserError::CannotBeCreated(_) => StatusCode::UNPROCESSABLE_ENTITY, - UserError::CannotBeUpdated(_, _) => StatusCode::UNPROCESSABLE_ENTITY, + UserError::CannotCreate(_) => StatusCode::UNPROCESSABLE_ENTITY, + UserError::CannotUpdate(_, _) => StatusCode::UNPROCESSABLE_ENTITY, + UserError::InvalidPayload(_) => StatusCode::UNPROCESSABLE_ENTITY, }; let message = self.to_string(); diff --git a/src/api/users/handlers.rs b/src/api/users/handlers.rs index 698eca0..21ac3e2 100644 --- a/src/api/users/handlers.rs +++ b/src/api/users/handlers.rs @@ -1,10 +1,13 @@ +use bytes::Buf; use warp::{ http::StatusCode, + reject, reject::Rejection, reply::{json, with_status, Reply}, }; use crate::types::PaginationParams; +use log::{error, info, warn}; use super::{ db::DBUser, @@ -28,15 +31,32 @@ pub async fn all_handler( } pub async fn create_handler( - user: NewUser, + buf: impl Buf, db_access: impl DBUser, ) -> Result { + let des = &mut serde_json::Deserializer::from_reader(buf.reader()); + let user: NewUser = serde_path_to_error::deserialize(des).map_err(|e| { + let e = e.to_string(); + warn!("invalid user '{e}'",); + reject::custom(UserError::InvalidPayload(e)) + })?; match db_access.by_username(&user.username)? { - Some(r) => Err(warp::reject::custom(UserError::AlreadyExists(r.id))), - None => Ok(with_status( - json(&db_access.create(&user)?), - StatusCode::CREATED, - )), + Some(user) => { + warn!("user already exists '{:?}'", user); + Err(warp::reject::custom(UserError::AlreadyExists(user.id))) + } + None => match db_access.create(&user) { + Ok(user) => { + info!("user '{}' created", user.username); + Ok(with_status(json(&user), StatusCode::CREATED)) + } + Err(error) => { + error!("error creating the user '{:?}': {}", user, error); + Err(warp::reject::custom(UserError::CannotCreate( + "error creating the user".to_string(), + ))) + } + }, } } pub async fn update_handler( @@ -54,7 +74,10 @@ pub async fn update_handler( } pub async fn delete_handler(id: i32, db_access: impl DBUser) -> Result { match db_access.by_id(id)? { - Some(p) => Ok(with_status(json(&db_access.delete(p.id)?), StatusCode::OK)), + Some(p) => Ok(with_status( + json(&db_access.delete(p.id)?), + StatusCode::NO_CONTENT, + )), None => Err(warp::reject::custom(UserError::NotFound(id))), } } diff --git a/src/api/users/routes.rs b/src/api/users/routes.rs index 3964910..20762a6 100644 --- a/src/api/users/routes.rs +++ b/src/api/users/routes.rs @@ -34,7 +34,7 @@ pub fn routes(db_access: impl DBUser) -> BoxedFilter<(impl Reply,)> { let create_user = user .and(with_auth()) .and(warp::post()) - .and(warp::body::json()) + .and(warp::body::aggregate()) .and(with_db(db_access.clone())) .and_then(handlers::create_handler); diff --git a/src/errors.rs b/src/errors.rs index f60a293..61ea21f 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -4,14 +4,9 @@ use std::convert::Infallible; use warp::{hyper::StatusCode, Rejection, Reply}; use crate::{ - auth::errors::AuthenticationError, - api::issues::errors::IssueError, - api::repositories::errors::RepositoryError, - api::projects::errors::ProjectError, - db::errors::DBError, - // pagination::{PaginationError, SortError}, - // repository::{errors::RepositoryError, models::RepositorySortError}, - // user::{errors::UserError, models::UserSortError}, + api::issues::errors::IssueError, api::projects::errors::ProjectError, + api::repositories::errors::RepositoryError, api::users::errors::UserError, + auth::errors::AuthenticationError, db::errors::DBError, }; #[derive(Serialize, Deserialize, PartialEq, Debug)] @@ -24,7 +19,9 @@ pub async fn error_handler(err: Rejection) -> std::result::Result() { return Ok(e.clone().into_response()); - }else if let Some(e) = err.find::() { + } else if let Some(e) = err.find::() { + return Ok(e.clone().into_response()); + } else if let Some(e) = err.find::() { return Ok(e.clone().into_response()); } // TODO: add more errors @@ -73,4 +70,4 @@ pub async fn error_handler(err: Rejection) -> std::result::Result Date: Sun, 8 Sep 2024 16:37:19 -0300 Subject: [PATCH 88/98] feat: add new endpoints to patch/delete issues assignees --- src/api/issues/db.rs | 19 +++++-- src/api/issues/errors.rs | 5 +- src/api/issues/handlers.rs | 106 ++++++++++++++++++++++++++++--------- src/api/issues/models.rs | 25 ++++++--- src/api/issues/routes.rs | 29 +++++++--- 5 files changed, 141 insertions(+), 43 deletions(-) diff --git a/src/api/issues/db.rs b/src/api/issues/db.rs index 65ef8b1..c33c94a 100644 --- a/src/api/issues/db.rs +++ b/src/api/issues/db.rs @@ -1,5 +1,6 @@ use diesel::dsl::now; use diesel::prelude::*; +use diesel::sql_query; use super::models::{Issue, NewIssue, QueryParams, UpdateIssue}; use crate::schema::issues::dsl as issues_dsl; @@ -23,6 +24,7 @@ pub trait DBIssue: Send + Sync + Clone + 'static { fn by_number(&self, repository_id: i32, number: i32) -> Result, DBError>; fn create(&self, issue: &NewIssue) -> Result; fn update(&self, id: i32, issue: &UpdateIssue) -> Result; + fn delete_issue_assignee(&self, id: i32) -> Result<(), DBError>; fn delete(&self, id: i32) -> Result<(), DBError>; } @@ -124,23 +126,32 @@ impl DBIssue for DBAccess { } fn create(&self, form: &NewIssue) -> Result { let conn = &mut self.get_db_conn(); - let project = diesel::insert_into(issues_dsl::issues) + let issue = diesel::insert_into(issues_dsl::issues) .values(form) .get_result(conn) .map_err(DBError::from)?; - Ok(project) + Ok(issue) } fn update(&self, id: i32, issue: &UpdateIssue) -> Result { let conn = &mut self.get_db_conn(); - let project = diesel::update(issues_dsl::issues.filter(issues_dsl::id.eq(id))) + let issue = diesel::update(issues_dsl::issues.filter(issues_dsl::id.eq(id))) .set((issue, issues_dsl::updated_at.eq(now))) .get_result::(conn) .map_err(DBError::from)?; - Ok(project) + Ok(issue) + } + fn delete_issue_assignee(&self, id: i32) -> Result<(), DBError> { + let conn = &mut self.get_db_conn(); + let query = + format!("UPDATE issues SET assignee_id = NULL, updated_at = now() WHERE id = {id}"); + + sql_query(query).execute(conn).map_err(DBError::from)?; + + Ok(()) } fn delete(&self, id: i32) -> Result<(), DBError> { diff --git a/src/api/issues/errors.rs b/src/api/issues/errors.rs index b070cb6..092a100 100644 --- a/src/api/issues/errors.rs +++ b/src/api/issues/errors.rs @@ -17,6 +17,7 @@ pub enum IssueError { RepositoryNotFound(i32), InvalidPayload(String), CannotCreate(String), + CannotUpdate(String), } impl fmt::Display for IssueError { @@ -26,7 +27,8 @@ impl fmt::Display for IssueError { IssueError::NotFound(id) => write!(f, "Issue #{id} not found"), IssueError::InvalidPayload(error) => write!(f, "Invalid payload: {error}"), IssueError::RepositoryNotFound(id) => write!(f, "Repository #{id} not found"), - IssueError::CannotCreate(error) => write!(f, "error creating the issue: {error}"), + IssueError::CannotCreate(error) => write!(f, "error creating the issue: {error}"), + IssueError::CannotUpdate(error) => write!(f, "error updating the issue: {error}"), } } } @@ -40,6 +42,7 @@ impl Reply for IssueError { IssueError::NotFound(_) => StatusCode::NOT_FOUND, IssueError::InvalidPayload(_) => StatusCode::UNPROCESSABLE_ENTITY, IssueError::CannotCreate(_) => StatusCode::INTERNAL_SERVER_ERROR, + IssueError::CannotUpdate(_) => StatusCode::INTERNAL_SERVER_ERROR, IssueError::RepositoryNotFound(_) => StatusCode::UNPROCESSABLE_ENTITY, }; let message = self.to_string(); diff --git a/src/api/issues/handlers.rs b/src/api/issues/handlers.rs index ceb7274..b2e5398 100644 --- a/src/api/issues/handlers.rs +++ b/src/api/issues/handlers.rs @@ -8,14 +8,14 @@ use warp::{ }; use crate::{ - api::repositories::db::DBRepository, + api::{repositories::db::DBRepository, users::db::DBUser}, types::{PaginatedResponse, PaginationParams}, }; use super::{ db::DBIssue, errors::IssueError, - models::{NewIssue, QueryParams, UpdateIssue}, + models::{IssueAssignee, NewIssue, QueryParams, UpdateIssue}, }; pub async fn by_id(id: i32, db_access: impl DBIssue) -> Result { @@ -81,7 +81,7 @@ pub async fn create_handler( None => { warn!("repository '{}' invalid", issue.repository_id); Err(warp::reject::custom(IssueError::RepositoryNotFound( - issue.repository_id + issue.repository_id, ))) } }, @@ -92,31 +92,33 @@ pub async fn create_handler( } pub async fn update_handler( id: i32, - issue: UpdateIssue, - db_access: impl DBIssue + DBRepository, + buf: impl Buf, + db_access: impl DBIssue, ) -> Result { - if let Some(repo_id) = issue.repository_id { - if DBRepository::by_id(&db_access, repo_id).is_err() { - return Err(warp::reject::custom(IssueError::InvalidPayload( - "invalid".to_owned(), - ))); - } + let des = &mut serde_json::Deserializer::from_reader(buf.reader()); + let issue: UpdateIssue = serde_path_to_error::deserialize(des).map_err(|e| { + let e = e.to_string(); + warn!("invalid issue update: '{e}'",); + reject::custom(IssueError::InvalidPayload(e)) + })?; + if !issue.has_any_field() { + let e = "all the fields are empty"; + warn!("invalid issue update: '{e}'",); + return Err(reject::custom(IssueError::InvalidPayload(e.to_string()))); } - match DBIssue::by_id(&db_access, id)? { - Some(p) => { - if let Some(repo_id) = issue.repository_id { - if DBRepository::by_id(&db_access, repo_id)?.is_none() { - return Err(warp::reject::custom(IssueError::RepositoryNotFound( - repo_id, - ))); - } + Some(p) => match db_access.update(p.id, &issue) { + Ok(issue) => { + info!("issue '{}' updated", issue.id); + Ok(with_status(json(&issue), StatusCode::OK)) } - Ok(with_status( - json(&DBIssue::update(&db_access, p.id, &issue)?), - StatusCode::OK, - )) - } + Err(error) => { + error!("error updating the issue '{:?}': {}", issue, error); + Err(warp::reject::custom(IssueError::CannotUpdate( + "error updating the issue".to_owned(), + ))) + } + }, None => Err(warp::reject::custom(IssueError::NotFound(id))), } } @@ -124,8 +126,62 @@ pub async fn delete_handler(id: i32, db_access: impl DBIssue) -> Result { let _ = &db_access.delete(id)?; - Ok(StatusCode::OK) + Ok(StatusCode::NO_CONTENT) } None => Err(warp::reject::custom(IssueError::NotFound(id)))?, } } + +pub async fn update_asignee_handler( + id: i32, + buf: impl Buf, + db_access: impl DBIssue + DBUser, +) -> Result { + let des = &mut serde_json::Deserializer::from_reader(buf.reader()); + let assignee: IssueAssignee = serde_path_to_error::deserialize(des).map_err(|e| { + let e = e.to_string(); + warn!("invalid issue assignee: '{e}'",); + reject::custom(IssueError::InvalidPayload(e)) + })?; + + match DBUser::by_username(&db_access, &assignee.username)? { + Some(u) => { + let update_issue = UpdateIssue { + assignee_id: Some(u.id), + ..Default::default() + }; + match DBIssue::update(&db_access, id, &update_issue) { + Ok(issue) => { + info!("issue '{}' assignee '{}' updated", issue.id, u.id); + Ok(with_status(json(&issue), StatusCode::OK)) + } + Err(error) => { + error!("error updating the issue '{id}': {error}"); + Err(warp::reject::custom(IssueError::CannotUpdate( + "error updating the issue assignee".to_owned(), + ))) + } + } + } + None => Err(warp::reject::custom(IssueError::InvalidPayload( + "username not found".to_string(), + ))), + } +} +pub async fn delete_asignee_handler( + id: i32, + db_access: impl DBIssue + DBUser, +) -> Result { + match db_access.delete_issue_assignee(id) { + Ok(issue) => { + info!("issue '{}' assignee deleted", id); + Ok(with_status(json(&issue), StatusCode::NO_CONTENT)) + } + Err(error) => { + error!("error deleteing the issue '{id}' assignee: {error}"); + Err(warp::reject::custom(IssueError::CannotUpdate( + "error deleting the issue assignee".to_owned(), + ))) + } + } +} diff --git a/src/api/issues/models.rs b/src/api/issues/models.rs index 4f3896d..eec0e7f 100644 --- a/src/api/issues/models.rs +++ b/src/api/issues/models.rs @@ -36,18 +36,24 @@ pub struct NewIssue { pub issue_created_at: DateTime, } -#[derive(AsChangeset, Serialize, Deserialize, Debug)] +#[derive(AsChangeset, Serialize, Deserialize, Debug, Default)] #[diesel(table_name = issues)] pub struct UpdateIssue { - pub id: i32, - pub number: i32, - pub title: String, + pub title: Option, pub labels: Option>, - pub open: bool, + pub open: Option, pub certified: Option, - pub repository_id: Option, pub assignee_id: Option, - pub issue_created_at: DateTime, +} + +impl UpdateIssue { + pub fn has_any_field(&self) -> bool { + self.title.is_some() + || self.labels.is_some() + || self.open.is_some() + || self.certified.is_some() + || self.assignee_id.is_some() + } } #[derive(Deserialize, Debug)] @@ -63,3 +69,8 @@ pub struct QueryParams { pub assignee_id: Option, pub open: Option, } + +#[derive(Serialize, Deserialize, Debug)] +pub struct IssueAssignee { + pub username: String, +} diff --git a/src/api/issues/routes.rs b/src/api/issues/routes.rs index fc479f6..6ff3217 100644 --- a/src/api/issues/routes.rs +++ b/src/api/issues/routes.rs @@ -4,6 +4,7 @@ use warp::filters::BoxedFilter; use warp::{Filter, Reply}; use crate::api::repositories::db::DBRepository; +use crate::api::users::db::DBUser; use crate::auth::with_auth; use crate::types::PaginationParams; @@ -12,14 +13,15 @@ use super::handlers; use super::models::QueryParams; fn with_db( - db_pool: impl DBIssue + DBRepository, -) -> impl Filter + Clone { + db_pool: impl DBIssue + DBRepository + DBUser, +) -> impl Filter + Clone { warp::any().map(move || db_pool.clone()) } -pub fn routes(db_access: impl DBIssue + DBRepository) -> BoxedFilter<(impl Reply,)> { +pub fn routes(db_access: impl DBIssue + DBRepository + DBUser) -> BoxedFilter<(impl Reply,)> { let issue = warp::path!("issues"); let issue_id = warp::path!("issues" / i32); + let issue_id_assignee = warp::path!("issues" / i32 / "assignee"); let get_issues = issue .and(warp::get()) @@ -48,16 +50,31 @@ pub fn routes(db_access: impl DBIssue + DBRepository) -> BoxedFilter<(impl Reply let update_issue = issue_id .and(with_auth()) - .and(warp::patch()) - .and(warp::body::json()) + .and(warp::put()) + .and(warp::body::aggregate()) .and(with_db(db_access.clone())) .and_then(handlers::update_handler); + let update_issue_assignee = issue_id_assignee + .and(with_auth()) + .and(warp::patch()) + .and(warp::body::aggregate()) + .and(with_db(db_access.clone())) + .and_then(handlers::update_asignee_handler); + + let delete_issue_assignee = issue_id_assignee + .and(with_auth()) + .and(warp::delete()) + .and(with_db(db_access.clone())) + .and_then(handlers::delete_asignee_handler); + let route = get_issues .or(get_issue) .or(create_issue) .or(delete_issue) - .or(update_issue); + .or(update_issue) + .or(update_issue_assignee) + .or(delete_issue_assignee); route.boxed() } From 1c2b25665a5948c3ab42e07d149e5a54c8db968d Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 8 Sep 2024 16:37:32 -0300 Subject: [PATCH 89/98] feat: add users in mock data script --- mock_data.bash | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/mock_data.bash b/mock_data.bash index 53751e3..d778afd 100644 --- a/mock_data.bash +++ b/mock_data.bash @@ -92,4 +92,27 @@ curl --location "$BASE_URL/issues" \ "certified": false, "repository_id": 3, "issue_created_at": "2024-09-03T09:13:54Z" +}' + +# Cretae users + +curl --location "$BASE_URL/users" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "username": "leapalazzolo" +}' + +curl --location "$BASE_URL/users" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "username": "CJ13th" +}' + +curl --location "$BASE_URL/users" \ +--header "$AUTH_HEADER" \ +--header "$CONTENT_TYPE_HEADER" \ +--data '{ + "username": "ipapandinas" }' \ No newline at end of file From 042d4bdc5b952c396c012e0519c11185f606f439 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 8 Sep 2024 16:38:09 -0300 Subject: [PATCH 90/98] chore: format code --- src/api/projects/errors.rs | 2 +- src/api/projects/handlers.rs | 26 +++++++++++------------ src/api/repositories/errors.rs | 7 ++++--- src/api/repositories/handlers.rs | 36 +++++++++++++++++++------------- src/main.rs | 1 - src/schema.rs | 8 +------ src/utils.rs | 6 +++++- 7 files changed, 46 insertions(+), 40 deletions(-) diff --git a/src/api/projects/errors.rs b/src/api/projects/errors.rs index 904831f..7ff34c1 100644 --- a/src/api/projects/errors.rs +++ b/src/api/projects/errors.rs @@ -23,7 +23,7 @@ impl fmt::Display for ProjectError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ProjectError::AlreadyExists(id) => write!(f, "Project #{id} already exists"), - ProjectError::NotFound(id) => write!(f, "Project #{id} not found"), + ProjectError::NotFound(id) => write!(f, "Project #{id} not found"), ProjectError::NotFoundBySlug(slug) => write!(f, "Project {slug} not found"), ProjectError::InvalidPayload(error) => write!(f, "Invalid payload: {error}"), ProjectError::CannotCreate(error) => write!(f, "Cannot create the project: {error}"), diff --git a/src/api/projects/handlers.rs b/src/api/projects/handlers.rs index 208e2c5..380c63c 100644 --- a/src/api/projects/handlers.rs +++ b/src/api/projects/handlers.rs @@ -5,14 +5,14 @@ use super::{ errors::ProjectError, models::{NewProject, QueryParams, UpdateProject}, }; +use bytes::Buf; +use log::{error, info, warn}; use warp::{ http::StatusCode, - reject::Rejection, reject, + reject::Rejection, reply::{json, with_status, Reply}, }; -use log::{error, info, warn}; -use bytes::Buf; pub async fn all_handler( db_access: impl DBProject, @@ -47,20 +47,17 @@ pub async fn create_handler( Some(p) => Err(warp::reject::custom(ProjectError::AlreadyExists(p.id))), None => match db_access.create(&project) { Ok(project) => { - info!("project slug '{}' created", project.slug); - Ok(with_status( - json(&project), - StatusCode::CREATED, - )) - }, + info!("project slug '{}' created", project.slug); + Ok(with_status(json(&project), StatusCode::CREATED)) + } Err(error) => { error!("error creating the project '{:?}': {}", project, error); Err(warp::reject::custom(ProjectError::CannotCreate( "error creating the project".to_string(), ))) - }, - }, - } + } + }, + } } pub async fn update_handler( @@ -79,7 +76,10 @@ pub async fn update_handler( pub async fn delete_handler(id: i32, db_access: impl DBProject) -> Result { match db_access.by_id(id)? { - Some(p) => Ok(with_status(json(&db_access.delete(p.id)?), StatusCode::OK)), + Some(p) => Ok(with_status( + json(&db_access.delete(p.id)?), + StatusCode::NO_CONTENT, + )), None => Err(warp::reject::custom(ProjectError::NotFound(id))), } } diff --git a/src/api/repositories/errors.rs b/src/api/repositories/errors.rs index fd40dfb..8741419 100644 --- a/src/api/repositories/errors.rs +++ b/src/api/repositories/errors.rs @@ -34,12 +34,13 @@ impl fmt::Display for RepositoryError { } RepositoryError::InvalidPayload(error) => { write!(f, "Invalid payload: {error}") - }, + } RepositoryError::CannotCreate(err) => { write!(f, "error creating the repository: {err}") - }, + } RepositoryError::ProjectNotFound(id) => { - write!(f, "Project id '{id}' does not exist")}, + write!(f, "Project id '{id}' does not exist") + } } } } diff --git a/src/api/repositories/handlers.rs b/src/api/repositories/handlers.rs index 293f8d4..b3b43b4 100644 --- a/src/api/repositories/handlers.rs +++ b/src/api/repositories/handlers.rs @@ -1,13 +1,16 @@ +use bytes::Buf; +use log::{error, info, warn}; use warp::{ http::StatusCode, - reject::Rejection, reject, + reject::Rejection, reply::{json, with_status, Reply}, }; -use log::{error, info, warn}; -use bytes::Buf; -use crate::{api::projects::db::DBProject, types::{PaginatedResponse, PaginationParams}}; +use crate::{ + api::projects::db::DBProject, + types::{PaginatedResponse, PaginationParams}, +}; use super::{ db::DBRepository, @@ -46,7 +49,6 @@ pub async fn create_handler( buf: impl Buf, db_access: impl DBRepository + DBProject, ) -> Result { - let des = &mut serde_json::Deserializer::from_reader(buf.reader()); let repository: NewRepository = serde_path_to_error::deserialize(des).map_err(|e| { let e = e.to_string(); @@ -55,28 +57,31 @@ pub async fn create_handler( })?; match DBRepository::by_slug(&db_access, &repository.slug)? { Some(r) => Err(warp::reject::custom(RepositoryError::AlreadyExists(r.id))), - None => match DBProject::by_id(&db_access, repository.project_id) { + None => match DBProject::by_id(&db_access, repository.project_id) { Ok(project) => match project { - Some(_) => match DBRepository::create(&db_access, &repository){ + Some(_) => match DBRepository::create(&db_access, &repository) { Ok(_) => { info!("repository slug '{}' created", repository.slug); - Ok(with_status(json(&repository), StatusCode::CREATED))}, - Err(err) => { + Ok(with_status(json(&repository), StatusCode::CREATED)) + } + Err(err) => { error!("error creating the repository '{:?}': {}", repository, err); Err(warp::reject::custom(RepositoryError::CannotCreate( "error creating the repository".to_owned(), ))) - }, + } }, None => { warn!("project id '{}' does not exist", repository.project_id); - Err(warp::reject::custom(RepositoryError::ProjectNotFound(repository.project_id))) - }, + Err(warp::reject::custom(RepositoryError::ProjectNotFound( + repository.project_id, + ))) + } }, Err(_) => Err(warp::reject::custom(RepositoryError::CannotCreate( "cannot check if the repository exists".to_owned(), ))), - } + }, } } pub async fn update_handler( @@ -97,7 +102,10 @@ pub async fn delete_handler( db_access: impl DBRepository, ) -> Result { match db_access.by_id(id)? { - Some(p) => Ok(with_status(json(&db_access.delete(p.id)?), StatusCode::OK)), + Some(p) => Ok(with_status( + json(&db_access.delete(p.id)?), + StatusCode::NO_CONTENT, + )), None => Err(warp::reject::custom(RepositoryError::NotFound(id))), } } diff --git a/src/main.rs b/src/main.rs index 72d076c..636ce9c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ - mod types; use log::{info, warn}; diff --git a/src/schema.rs b/src/schema.rs index ce84804..1eb427f 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -74,10 +74,4 @@ diesel::joinable!(issues -> repositories (repository_id)); diesel::joinable!(issues -> users (assignee_id)); diesel::joinable!(repositories -> projects (project_id)); -diesel::allow_tables_to_appear_in_same_query!( - issues, - languages, - projects, - repositories, - users, -); +diesel::allow_tables_to_appear_in_same_query!(issues, languages, projects, repositories, users,); diff --git a/src/utils.rs b/src/utils.rs index ca1f706..2d49c14 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -29,7 +29,11 @@ pub fn setup_filters(db: DBAccess) -> BoxedFilter<(impl Reply,)> { .or(repositories_route) .or(issues_route) .or(users_route) - .with(warp::cors().allow_any_origin().allow_header("Authorization")) //TODO: restrict url + .with( + warp::cors() + .allow_any_origin() + .allow_header("Authorization"), + ) //TODO: restrict url .recover(error_handler) .boxed() } From 7d56b0ad4c9c70bee98627e72fb07ec79314bd27 Mon Sep 17 00:00:00 2001 From: Connor Campbell Date: Thu, 12 Sep 2024 10:28:34 +0200 Subject: [PATCH 91/98] refactor: use text cols + add types/remove categories --- migrations/2024-04-13-204151_init/up.sql | 20 ++++++------- src/schema.rs | 37 +++++++++++------------- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/migrations/2024-04-13-204151_init/up.sql b/migrations/2024-04-13-204151_init/up.sql index 29a4ed3..26bdf33 100644 --- a/migrations/2024-04-13-204151_init/up.sql +++ b/migrations/2024-04-13-204151_init/up.sql @@ -1,9 +1,9 @@ -- basic github repository CREATE TABLE IF NOT EXISTS projects ( id SERIAL PRIMARY KEY, - name VARCHAR(255) NOT NULL, - slug VARCHAR(255) NOT NULL UNIQUE, - categories TEXT [], + name TEXT NOT NULL, + slug TEXT NOT NULL UNIQUE, + types TEXT [], purposes TEXT [], stack_levels TEXT [], technologies TEXT [], @@ -12,17 +12,17 @@ CREATE TABLE IF NOT EXISTS projects ( ); CREATE TABLE IF NOT EXISTS languages ( id SERIAL PRIMARY KEY, - slug VARCHAR(255) NOT NULL UNIQUE, + slug TEXT NOT NULL UNIQUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NULL ); -- basic github repository CREATE TABLE IF NOT EXISTS repositories ( id SERIAL PRIMARY KEY, - slug VARCHAR(255) NOT NULL UNIQUE, - name VARCHAR(255) NOT NULL, - url VARCHAR(255) NOT NULL, - language_slug VARCHAR(255) NOT NULL, + slug TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + url TEXT NOT NULL, + language_slug TEXT NOT NULL, project_id INT REFERENCES projects(id) ON DELETE CASCADE NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NULL @@ -30,7 +30,7 @@ CREATE TABLE IF NOT EXISTS repositories ( -- all the users including maintainers and admins CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, - username VARCHAR(255) UNIQUE NOT NULL, + username TEXT UNIQUE NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NULL ); @@ -38,7 +38,7 @@ CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS issues ( id SERIAL PRIMARY KEY, number int NOT NULL, - title VARCHAR(255) NOT NULL, + title TEXT NOT NULL, labels TEXT [], open boolean DEFAULT true NOT NULL, certified boolean, diff --git a/src/schema.rs b/src/schema.rs index 1eb427f..0e9191a 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -4,8 +4,7 @@ diesel::table! { issues (id) { id -> Int4, number -> Int4, - #[max_length = 255] - title -> Varchar, + title -> Text, labels -> Nullable>>, open -> Bool, certified -> Nullable, @@ -20,8 +19,7 @@ diesel::table! { diesel::table! { languages (id) { id -> Int4, - #[max_length = 255] - slug -> Varchar, + slug -> Text, created_at -> Timestamptz, updated_at -> Nullable, } @@ -30,11 +28,9 @@ diesel::table! { diesel::table! { projects (id) { id -> Int4, - #[max_length = 255] - name -> Varchar, - #[max_length = 255] - slug -> Varchar, - categories -> Nullable>>, + name -> Text, + slug -> Text, + types -> Nullable>>, purposes -> Nullable>>, stack_levels -> Nullable>>, technologies -> Nullable>>, @@ -46,14 +42,10 @@ diesel::table! { diesel::table! { repositories (id) { id -> Int4, - #[max_length = 255] - slug -> Varchar, - #[max_length = 255] - name -> Varchar, - #[max_length = 255] - url -> Varchar, - #[max_length = 255] - language_slug -> Varchar, + slug -> Text, + name -> Text, + url -> Text, + language_slug -> Text, project_id -> Int4, created_at -> Timestamptz, updated_at -> Nullable, @@ -63,8 +55,7 @@ diesel::table! { diesel::table! { users (id) { id -> Int4, - #[max_length = 255] - username -> Varchar, + username -> Text, created_at -> Timestamptz, updated_at -> Nullable, } @@ -74,4 +65,10 @@ diesel::joinable!(issues -> repositories (repository_id)); diesel::joinable!(issues -> users (assignee_id)); diesel::joinable!(repositories -> projects (project_id)); -diesel::allow_tables_to_appear_in_same_query!(issues, languages, projects, repositories, users,); +diesel::allow_tables_to_appear_in_same_query!( + issues, + languages, + projects, + repositories, + users, +); From 6252a424a3c9a91df5ea89a200797afa0f42e340 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 15 Sep 2024 17:16:11 -0300 Subject: [PATCH 92/98] chore: remove stage cron --- .github/workflows/stage_cron.yaml | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 .github/workflows/stage_cron.yaml diff --git a/.github/workflows/stage_cron.yaml b/.github/workflows/stage_cron.yaml deleted file mode 100644 index 7043943..0000000 --- a/.github/workflows/stage_cron.yaml +++ /dev/null @@ -1,12 +0,0 @@ -name: Stage cron -on: - schedule: - - cron: "*/15 * * * *" - -jobs: - stage: - runs-on: ubuntu-latest - steps: - - name: Wake up service - run: | - curl --fail ${{ vars.ISSUES_API_STAGE_URL }}/health From af305dcf243f9bfb28a9572a7c219b3a424e044e Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 15 Sep 2024 17:28:03 -0300 Subject: [PATCH 93/98] fix: add types --- src/api/issues/db.rs | 4 ---- src/api/issues/models.rs | 1 - src/api/projects/db.rs | 5 ----- src/api/projects/models.rs | 5 +---- 4 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/api/issues/db.rs b/src/api/issues/db.rs index c33c94a..dcdd0fb 100644 --- a/src/api/issues/db.rs +++ b/src/api/issues/db.rs @@ -55,10 +55,6 @@ impl DBIssue for DBAccess { query = query.filter(projects_dsl::slug.eq(slug)); } - if let Some(category) = params.categories.as_ref() { - query = query.filter(projects_dsl::categories.contains(vec![category])); - } - if let Some(purpose) = params.purposes.as_ref() { query = query.filter(projects_dsl::purposes.contains(vec![purpose])); } diff --git a/src/api/issues/models.rs b/src/api/issues/models.rs index eec0e7f..917e9a1 100644 --- a/src/api/issues/models.rs +++ b/src/api/issues/models.rs @@ -59,7 +59,6 @@ impl UpdateIssue { #[derive(Deserialize, Debug)] pub struct QueryParams { pub slug: Option, - pub categories: Option, pub purposes: Option, pub stack_levels: Option, pub technologies: Option, diff --git a/src/api/projects/db.rs b/src/api/projects/db.rs index 4c23cf7..93744c4 100644 --- a/src/api/projects/db.rs +++ b/src/api/projects/db.rs @@ -39,11 +39,6 @@ impl DBProject for DBAccess { query = query.filter(projects_dsl::slug.eq(slug)); } - if let Some(raw_categories) = params.categories.as_ref() { - let categories: Vec = utils::parse_comma_values(raw_categories); - query = query.filter(projects_dsl::categories.overlaps_with(categories)); - } - if let Some(raw_purposes) = params.purposes.as_ref() { let purposes: Vec = utils::parse_comma_values(raw_purposes); query = query.filter(projects_dsl::purposes.overlaps_with(purposes)); diff --git a/src/api/projects/models.rs b/src/api/projects/models.rs index cdd6bc2..01c5aa0 100644 --- a/src/api/projects/models.rs +++ b/src/api/projects/models.rs @@ -13,7 +13,7 @@ pub struct Project { pub id: i32, pub name: String, pub slug: String, - pub categories: Option>>, + pub types: Option>>, pub purposes: Option>>, pub stack_levels: Option>>, pub technologies: Option>>, @@ -24,7 +24,6 @@ pub struct Project { #[derive(Deserialize, Debug)] pub struct QueryParams { pub slug: Option, - pub categories: Option, pub purposes: Option, pub stack_levels: Option, pub technologies: Option, @@ -35,7 +34,6 @@ pub struct QueryParams { pub struct NewProject { pub name: String, pub slug: String, - pub categories: Option>>, pub purposes: Option>>, pub stack_levels: Option>>, pub technologies: Option>>, @@ -46,7 +44,6 @@ pub struct NewProject { pub struct UpdateProject { pub name: Option, pub slug: Option, - pub categories: Option>>, pub purposes: Option>>, pub stack_levels: Option>>, pub technologies: Option>>, From fe1fb3d8d28ed12f02a173f319341acf57888abb Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 15 Sep 2024 19:17:36 -0300 Subject: [PATCH 94/98] feat: add closed_at issue --- migrations/2024-04-13-204151_init/up.sql | 6 ++++++ src/api/issues/models.rs | 3 +++ src/schema.rs | 1 + 3 files changed, 10 insertions(+) diff --git a/migrations/2024-04-13-204151_init/up.sql b/migrations/2024-04-13-204151_init/up.sql index 26bdf33..3636ef9 100644 --- a/migrations/2024-04-13-204151_init/up.sql +++ b/migrations/2024-04-13-204151_init/up.sql @@ -45,6 +45,12 @@ CREATE TABLE IF NOT EXISTS issues ( assignee_id INT REFERENCES users(id) NULL, repository_id INT REFERENCES repositories(id) ON DELETE CASCADE NOT NULL, issue_created_at TIMESTAMP WITH TIME ZONE NOT NULL, + issue_closed_at TIMESTAMP WITH TIME ZONE NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NULL + CONSTRAINT issue_closed_at_check CHECK ( + issue_closed_at IS NULL OR ( + issue_closed_at > issue_created_at + ) + ) ); \ No newline at end of file diff --git a/src/api/issues/models.rs b/src/api/issues/models.rs index 917e9a1..445e875 100644 --- a/src/api/issues/models.rs +++ b/src/api/issues/models.rs @@ -19,6 +19,7 @@ pub struct Issue { pub assignee_id: Option, pub repository_id: i32, pub issue_created_at: DateTime, + pub issue_closed_at: Option>, pub created_at: DateTime, pub updated_at: Option>, } @@ -44,6 +45,7 @@ pub struct UpdateIssue { pub open: Option, pub certified: Option, pub assignee_id: Option, + pub issue_closed_at: Option>, } impl UpdateIssue { @@ -53,6 +55,7 @@ impl UpdateIssue { || self.open.is_some() || self.certified.is_some() || self.assignee_id.is_some() + || self.issue_closed_at.is_some() } } diff --git a/src/schema.rs b/src/schema.rs index 0e9191a..405c90f 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -11,6 +11,7 @@ diesel::table! { assignee_id -> Nullable, repository_id -> Int4, issue_created_at -> Timestamptz, + issue_closed_at -> Nullable, created_at -> Timestamptz, updated_at -> Nullable, } From 17ae6faa8231662c2f814907de71a99b2d0955bc Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 15 Sep 2024 20:25:59 -0300 Subject: [PATCH 95/98] feat: return error if issue_closed_at is lower than issue_created_at --- src/api/issues/handlers.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/api/issues/handlers.rs b/src/api/issues/handlers.rs index b2e5398..1673ea1 100644 --- a/src/api/issues/handlers.rs +++ b/src/api/issues/handlers.rs @@ -114,9 +114,15 @@ pub async fn update_handler( } Err(error) => { error!("error updating the issue '{:?}': {}", issue, error); - Err(warp::reject::custom(IssueError::CannotUpdate( - "error updating the issue".to_owned(), - ))) + if error.to_string().contains("issue_closed_at_check") { + Err(warp::reject::custom(IssueError::InvalidPayload( + "issue_closed_at is lower than issue_created_at".to_owned(), + ))) + } else { + Err(warp::reject::custom(IssueError::CannotUpdate( + "error updating the issue".to_owned(), + ))) + } } }, None => Err(warp::reject::custom(IssueError::NotFound(id))), From 04540d7f03d7420c1f052f036a168734b3a04b42 Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Sun, 15 Sep 2024 20:29:02 -0300 Subject: [PATCH 96/98] feat: add username in issues endpoint --- src/api/issues/db.rs | 60 +++++++++++++++++++++++++++++++++++++--- src/api/issues/models.rs | 22 ++++++++++++++- 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/src/api/issues/db.rs b/src/api/issues/db.rs index dcdd0fb..a86292f 100644 --- a/src/api/issues/db.rs +++ b/src/api/issues/db.rs @@ -2,11 +2,13 @@ use diesel::dsl::now; use diesel::prelude::*; use diesel::sql_query; +use super::models::IssueWithUsername; use super::models::{Issue, NewIssue, QueryParams, UpdateIssue}; use crate::schema::issues::dsl as issues_dsl; use crate::schema::languages::dsl as languages_dsl; use crate::schema::projects::dsl as projects_dsl; use crate::schema::repositories::dsl as repositories_dsl; +use crate::schema::users::dsl as users_dsl; use crate::db::{ errors::DBError, @@ -19,7 +21,7 @@ pub trait DBIssue: Send + Sync + Clone + 'static { &self, params: QueryParams, pagination: PaginationParams, - ) -> Result<(Vec, i64), DBError>; + ) -> Result<(Vec, i64), DBError>; fn by_id(&self, id: i32) -> Result, DBError>; fn by_number(&self, repository_id: i32, number: i32) -> Result, DBError>; fn create(&self, issue: &NewIssue) -> Result; @@ -33,7 +35,7 @@ impl DBIssue for DBAccess { &self, params: QueryParams, pagination: PaginationParams, - ) -> Result<(Vec, i64), DBError> { + ) -> Result<(Vec, i64), DBError> { let conn = &mut self.get_db_conn(); let build_query = || { @@ -49,6 +51,9 @@ impl DBIssue for DBAccess { languages_dsl::languages .on(repositories_dsl::language_slug.eq(languages_dsl::slug)), ) + .left_join( + users_dsl::users.on(issues_dsl::assignee_id.eq(users_dsl::id.nullable())), + ) .into_boxed(); if let Some(slug) = params.slug.as_ref() { @@ -88,6 +93,30 @@ impl DBIssue for DBAccess { query = query.filter(issues_dsl::repository_id.eq(repository_id)); } + if let Some(has_assignee) = params.has_assignee.as_ref() { + if *has_assignee { + query = query.filter(issues_dsl::assignee_id.is_not_null()); + } else { + query = query.filter(issues_dsl::assignee_id.is_null()); + } + } + + if let Some(closed_at_min) = params.issue_closed_at_min.as_ref() { + query = query.filter( + issues_dsl::issue_closed_at + .is_not_null() + .and(issues_dsl::issue_closed_at.ge(closed_at_min)), + ); + } + + if let Some(closed_at_max) = params.issue_closed_at_max.as_ref() { + query = query.filter( + issues_dsl::issue_closed_at + .is_not_null() + .and(issues_dsl::issue_closed_at.le(closed_at_max)), + ); + } + query }; @@ -96,8 +125,31 @@ impl DBIssue for DBAccess { let result = build_query() .offset(pagination.offset) .limit(pagination.limit) - .select(issues_dsl::issues::all_columns()) - .load::(conn)?; + .select(( + issues_dsl::issues::all_columns(), + users_dsl::username.nullable(), + )) + .load::<(Issue, Option)>(conn) + .map(|results| { + results + .into_iter() + .map(|(issue, username)| IssueWithUsername { + id: issue.id, + number: issue.number, + title: issue.title, + labels: issue.labels, + open: issue.open, + certified: issue.certified, + assignee_id: issue.assignee_id, + assignee_username: username, + repository_id: issue.repository_id, + issue_created_at: issue.issue_created_at, + issue_closed_at: issue.issue_closed_at, + created_at: issue.created_at, + updated_at: issue.updated_at, + }) + .collect::>() + })?; Ok((result, total_count)) } diff --git a/src/api/issues/models.rs b/src/api/issues/models.rs index 445e875..ec11033 100644 --- a/src/api/issues/models.rs +++ b/src/api/issues/models.rs @@ -1,4 +1,4 @@ -use crate::schema::issues; +use crate::{api::users::models::User, schema::issues}; use chrono::{DateTime, Utc}; use diesel::prelude::*; @@ -24,6 +24,23 @@ pub struct Issue { pub updated_at: Option>, } +#[derive(Debug, Serialize, Deserialize, Queryable)] +pub struct IssueWithUsername { + pub id: i32, + pub number: i32, + pub title: String, + pub labels: Option>>, + pub open: bool, + pub certified: Option, + pub assignee_id: Option, + pub assignee_username: Option, + pub repository_id: i32, + pub issue_created_at: DateTime, + pub issue_closed_at: Option>, + pub created_at: DateTime, + pub updated_at: Option>, +} + #[derive(Insertable, Serialize, Deserialize, Debug)] #[diesel(table_name = issues)] pub struct NewIssue { @@ -70,6 +87,9 @@ pub struct QueryParams { pub repository_id: Option, pub assignee_id: Option, pub open: Option, + pub has_assignee: Option, + pub issue_closed_at_min: Option>, + pub issue_closed_at_max: Option>, } #[derive(Serialize, Deserialize, Debug)] From c8228f76c3e5babbd4ce04e637de36ab18469f2c Mon Sep 17 00:00:00 2001 From: Igor Papandinas <26460174+ipapandinas@users.noreply.github.com> Date: Wed, 18 Sep 2024 00:44:08 +0200 Subject: [PATCH 97/98] response types --- src/api/issues/db.rs | 79 ++++++++++++++++++++++------------ src/api/issues/models.rs | 36 ++++++++-------- src/api/projects/models.rs | 12 ++++++ src/api/repositories/models.rs | 14 +++++- 4 files changed, 95 insertions(+), 46 deletions(-) diff --git a/src/api/issues/db.rs b/src/api/issues/db.rs index a86292f..51aaac6 100644 --- a/src/api/issues/db.rs +++ b/src/api/issues/db.rs @@ -2,8 +2,11 @@ use diesel::dsl::now; use diesel::prelude::*; use diesel::sql_query; -use super::models::IssueWithUsername; -use super::models::{Issue, NewIssue, QueryParams, UpdateIssue}; +use super::models::{Issue, IssueResponse, NewIssue, QueryParams, UpdateIssue}; +use crate::api::projects::models::Project; +use crate::api::projects::models::ProjectResponse; +use crate::api::repositories::models::Repository; +use crate::api::repositories::models::RepositoryResponse; use crate::schema::issues::dsl as issues_dsl; use crate::schema::languages::dsl as languages_dsl; use crate::schema::projects::dsl as projects_dsl; @@ -21,7 +24,7 @@ pub trait DBIssue: Send + Sync + Clone + 'static { &self, params: QueryParams, pagination: PaginationParams, - ) -> Result<(Vec, i64), DBError>; + ) -> Result<(Vec, i64), DBError>; fn by_id(&self, id: i32) -> Result, DBError>; fn by_number(&self, repository_id: i32, number: i32) -> Result, DBError>; fn create(&self, issue: &NewIssue) -> Result; @@ -35,7 +38,7 @@ impl DBIssue for DBAccess { &self, params: QueryParams, pagination: PaginationParams, - ) -> Result<(Vec, i64), DBError> { + ) -> Result<(Vec, i64), DBError> { let conn = &mut self.get_db_conn(); let build_query = || { @@ -81,6 +84,10 @@ impl DBIssue for DBAccess { query = query.filter(issues_dsl::labels.overlaps_with(labels)); } + if let Some(certified) = params.certified.as_ref() { + query = query.filter(issues_dsl::certified.eq(certified)); + } + if let Some(open) = params.open.as_ref() { query = query.filter(issues_dsl::open.eq(open)); } @@ -127,31 +134,49 @@ impl DBIssue for DBAccess { .limit(pagination.limit) .select(( issues_dsl::issues::all_columns(), + repositories_dsl::repositories::all_columns(), + projects_dsl::projects::all_columns(), users_dsl::username.nullable(), )) - .load::<(Issue, Option)>(conn) - .map(|results| { - results - .into_iter() - .map(|(issue, username)| IssueWithUsername { - id: issue.id, - number: issue.number, - title: issue.title, - labels: issue.labels, - open: issue.open, - certified: issue.certified, - assignee_id: issue.assignee_id, - assignee_username: username, - repository_id: issue.repository_id, - issue_created_at: issue.issue_created_at, - issue_closed_at: issue.issue_closed_at, - created_at: issue.created_at, - updated_at: issue.updated_at, - }) - .collect::>() - })?; - - Ok((result, total_count)) + .load::<(Issue, Repository, Project, Option)>(conn)?; + + let issues_full = result + .into_iter() + .map(|(issue, repo, project, username)| IssueResponse { + id: issue.id, + issue_id: issue.number, + labels: issue.labels, + open: issue.open, + assignee_id: issue.assignee_id, + assignee_username: username, + title: issue.title, + certified: issue.certified.unwrap_or(false), + repository: RepositoryResponse { + id: repo.id, + slug: repo.slug, + name: repo.name, + url: repo.url, + language_slug: repo.language_slug, + project: ProjectResponse { + id: project.id, + name: project.name, + slug: project.slug, + purposes: project.purposes, + stack_levels: project.stack_levels, + technologies: project.technologies, + created_at: project.created_at, + updated_at: project.updated_at, + }, + created_at: repo.created_at, + updated_at: repo.updated_at, + }, + timestamp_created_at: issue.issue_created_at, + created_at: issue.created_at, + updated_at: issue.updated_at, + }) + .collect(); + + Ok((issues_full, total_count)) } fn by_id(&self, id: i32) -> Result, DBError> { let conn = &mut self.get_db_conn(); diff --git a/src/api/issues/models.rs b/src/api/issues/models.rs index ec11033..ae90f55 100644 --- a/src/api/issues/models.rs +++ b/src/api/issues/models.rs @@ -1,4 +1,4 @@ -use crate::{api::users::models::User, schema::issues}; +use crate::{api::repositories::models::RepositoryResponse, schema::issues}; use chrono::{DateTime, Utc}; use diesel::prelude::*; @@ -24,23 +24,6 @@ pub struct Issue { pub updated_at: Option>, } -#[derive(Debug, Serialize, Deserialize, Queryable)] -pub struct IssueWithUsername { - pub id: i32, - pub number: i32, - pub title: String, - pub labels: Option>>, - pub open: bool, - pub certified: Option, - pub assignee_id: Option, - pub assignee_username: Option, - pub repository_id: i32, - pub issue_created_at: DateTime, - pub issue_closed_at: Option>, - pub created_at: DateTime, - pub updated_at: Option>, -} - #[derive(Insertable, Serialize, Deserialize, Debug)] #[diesel(table_name = issues)] pub struct NewIssue { @@ -79,6 +62,7 @@ impl UpdateIssue { #[derive(Deserialize, Debug)] pub struct QueryParams { pub slug: Option, + pub certified: Option, pub purposes: Option, pub stack_levels: Option, pub technologies: Option, @@ -96,3 +80,19 @@ pub struct QueryParams { pub struct IssueAssignee { pub username: String, } + +#[derive(Serialize, Debug)] +pub struct IssueResponse { + pub id: i32, + pub issue_id: i32, + pub certified: bool, + pub title: String, + pub labels: Option>>, + pub open: bool, + pub assignee_id: Option, + pub assignee_username: Option, + pub repository: RepositoryResponse, + pub timestamp_created_at: DateTime, + pub created_at: DateTime, + pub updated_at: Option>, +} diff --git a/src/api/projects/models.rs b/src/api/projects/models.rs index 01c5aa0..2b516a7 100644 --- a/src/api/projects/models.rs +++ b/src/api/projects/models.rs @@ -48,3 +48,15 @@ pub struct UpdateProject { pub stack_levels: Option>>, pub technologies: Option>>, } + +#[derive(Serialize, Debug)] +pub struct ProjectResponse { + pub id: i32, + pub name: String, + pub slug: String, + pub purposes: Option>>, + pub stack_levels: Option>>, + pub technologies: Option>>, + pub created_at: DateTime, + pub updated_at: Option>, +} diff --git a/src/api/repositories/models.rs b/src/api/repositories/models.rs index 9087fc5..fddc19f 100644 --- a/src/api/repositories/models.rs +++ b/src/api/repositories/models.rs @@ -1,4 +1,4 @@ -use crate::schema::repositories; +use crate::{api::projects::models::ProjectResponse, schema::repositories}; use chrono::{DateTime, Utc}; use diesel::prelude::*; @@ -47,3 +47,15 @@ pub struct UpdateRepository { pub language_slug: Option, pub project_id: Option, } + +#[derive(Serialize, Debug)] +pub struct RepositoryResponse { + pub id: i32, + pub slug: String, + pub name: String, + pub url: String, + pub language_slug: String, + pub project: ProjectResponse, + pub created_at: DateTime, + pub updated_at: Option>, +} From 7e32a12d01eabf06ebbdf2d903ebda8cef925acf Mon Sep 17 00:00:00 2001 From: Leandro Palazzolo Date: Thu, 19 Sep 2024 09:21:28 -0300 Subject: [PATCH 98/98] fix: issues fields --- src/api/issues/db.rs | 3 ++- src/api/issues/models.rs | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/api/issues/db.rs b/src/api/issues/db.rs index 51aaac6..c4794ed 100644 --- a/src/api/issues/db.rs +++ b/src/api/issues/db.rs @@ -170,7 +170,8 @@ impl DBIssue for DBAccess { created_at: repo.created_at, updated_at: repo.updated_at, }, - timestamp_created_at: issue.issue_created_at, + issue_created_at: issue.issue_created_at, + issue_closed_at: issue.issue_closed_at, created_at: issue.created_at, updated_at: issue.updated_at, }) diff --git a/src/api/issues/models.rs b/src/api/issues/models.rs index ae90f55..5248944 100644 --- a/src/api/issues/models.rs +++ b/src/api/issues/models.rs @@ -85,14 +85,15 @@ pub struct IssueAssignee { pub struct IssueResponse { pub id: i32, pub issue_id: i32, - pub certified: bool, pub title: String, pub labels: Option>>, pub open: bool, + pub certified: bool, pub assignee_id: Option, pub assignee_username: Option, pub repository: RepositoryResponse, - pub timestamp_created_at: DateTime, + pub issue_created_at: DateTime, + pub issue_closed_at: Option>, pub created_at: DateTime, pub updated_at: Option>, }