Create a plan for implementing projects and also create the backend API. (#19)
Some checks failed
Check / Backend (push) Successful in 6m55s
Check / Frontend (push) Successful in 2m2s
Docker Build / Build and Push Backend Image (push) Failing after 11m21s
Docker Build / Build and Push Frontend Image (push) Successful in 5m44s

Creates projects to organize tasks under. (Note the migration also contains the tables for creating folders for projects as well because adding foreign keys is a PITA in sqlite apparently).

Reviewed-on: #19
Co-authored-by: Drew Galbraith <drew@tiramisu.one>
Co-committed-by: Drew Galbraith <drew@tiramisu.one>
This commit is contained in:
Drew 2025-10-28 04:13:12 +00:00 committed by Drew
parent 69f4a6f1ca
commit 4552c347c6
17 changed files with 957 additions and 12 deletions

View file

@ -1,3 +1,4 @@
mod projects;
mod tasks;
use axum::{
@ -26,11 +27,14 @@ pub struct ErrorJson {
impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
match self {
Self::InternalError(anyhow_error) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong {}", anyhow_error),
)
.into_response(),
Self::InternalError(anyhow_error) => {
error!(err = ?anyhow_error, "Internal Server Error");
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong {}", anyhow_error),
)
.into_response()
}
Self::JsonExtractError(rej) => (
StatusCode::UNPROCESSABLE_ENTITY,
Json(ErrorJson {
@ -85,5 +89,7 @@ impl From<sqlx::Error> for AppError {
}
}
pub use projects::*;
use serde::Serialize;
pub use tasks::*;
use tracing::error;

View file

@ -0,0 +1,106 @@
use axum::{
Json, Router,
extract::{Path, State},
http::StatusCode,
routing::{get, post},
};
use axum_extra::extract::WithRejection;
use serde::Deserialize;
use sqlx::{Pool, Sqlite};
use uuid::Uuid;
use crate::models::{ProjectModel, ProjectStatus};
use super::AppError;
pub fn create_project_router() -> Router<Pool<Sqlite>> {
Router::new()
.route("/", post(create_project).get(list_projects))
.route(
"/{project_id}",
get(get_project).put(update_project).delete(delete_project),
)
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CreateProjectRequest {
title: String,
color: String,
}
pub async fn create_project(
State(pool): State<Pool<Sqlite>>,
WithRejection(Json(input), _): WithRejection<Json<CreateProjectRequest>, AppError>,
) -> Result<(StatusCode, Json<ProjectModel>), AppError> {
Ok((
StatusCode::CREATED,
Json(ProjectModel::insert(&pool, input.title, input.color).await?),
))
}
pub async fn list_projects(
State(pool): State<Pool<Sqlite>>,
) -> Result<(StatusCode, Json<Vec<ProjectModel>>), AppError> {
Ok((StatusCode::OK, Json(ProjectModel::list(&pool).await?)))
}
pub async fn get_project(
State(pool): State<Pool<Sqlite>>,
WithRejection(Path(project_id), _): WithRejection<Path<Uuid>, AppError>,
) -> Result<(StatusCode, Json<ProjectModel>), AppError> {
Ok((
StatusCode::OK,
Json(ProjectModel::get_by_id(&pool, project_id).await?),
))
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct UpdateProjectRequest {
title: Option<String>,
color: Option<String>,
status: Option<ProjectStatus>,
}
pub async fn update_project(
State(pool): State<Pool<Sqlite>>,
WithRejection(Path(project_id), _): WithRejection<Path<Uuid>, AppError>,
WithRejection(Json(input), _): WithRejection<Json<UpdateProjectRequest>, AppError>,
) -> Result<(StatusCode, Json<ProjectModel>), AppError> {
let mut project = ProjectModel::get_by_id(&pool, project_id).await?;
if let Some(new_title) = input.title {
if new_title.len() == 0 {
return Err(AppError::Unprocessable(
"Title must not be empty".to_string(),
));
}
project.title = new_title;
}
if let Some(color) = input.color {
if color.len() != 7 || color.chars().next() != Some('#') {
return Err(AppError::Unprocessable(
"Color is not a valid hex".to_string(),
));
}
project.color = color;
}
if let Some(status) = input.status {
project.status = status;
}
Ok((StatusCode::OK, Json(project.update(&pool).await?)))
}
pub async fn delete_project(
State(pool): State<Pool<Sqlite>>,
WithRejection(Path(project_id), _): WithRejection<Path<Uuid>, AppError>,
) -> Result<StatusCode, AppError> {
ProjectModel::get_by_id(&pool, project_id).await?;
ProjectModel::delete(&pool, project_id).await?;
Ok(StatusCode::NO_CONTENT)
}