captains-log/backend/src/models/project.rs
Drew Galbraith d4b8302306
Some checks failed
Check / Backend (pull_request) Failing after 2m29s
Check / Frontend (pull_request) Successful in 1m55s
Create Project APIs.
2025-10-26 15:30:21 -07:00

122 lines
3.4 KiB
Rust

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{Pool, Row, Sqlite};
use uuid::Uuid;
use crate::services::AppError;
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy, sqlx::Type)]
#[serde(rename_all = "snake_case")]
#[sqlx(rename_all = "lowercase")]
pub enum ProjectStatus {
Active,
Done,
Backlog,
}
#[derive(sqlx::FromRow, Serialize)]
pub struct ProjectModel {
pub id: Uuid,
pub title: String,
pub color: String,
pub folder_id: Option<Uuid>,
pub sort_order: i64,
pub status: ProjectStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
}
impl ProjectModel {
async fn get_new_sort_order(pool: &Pool<Sqlite>) -> anyhow::Result<i64> {
let result = sqlx::query!("SELECT MAX(sort_order) as max_order FROM projects;")
.fetch_one(pool)
.await?;
match result.max_order {
Some(i) => Ok(i + 1000),
None => Ok(0),
}
}
pub async fn insert(
pool: &Pool<Sqlite>,
title: String,
color: String,
) -> anyhow::Result<ProjectModel, AppError> {
if title.is_empty() {
return Err(AppError::Unprocessable(
"Title must not be empty".to_string(),
));
}
if color.len() != 7 || color.chars().next() != Some('#') {
return Err(AppError::Unprocessable(
"Color is not a valid hex".to_string(),
));
}
let id = Uuid::new_v4();
let sort_order = ProjectModel::get_new_sort_order(pool).await?;
let result = sqlx::query_as(
"INSERT INTO projects(id, title, color, sort_order) VALUES($1, $2, $3, $4) RETURNING *",
)
.bind(id)
.bind(title)
.bind(color)
.bind(sort_order)
.fetch_one(pool)
.await?;
Ok(result)
}
pub async fn get_by_id(
pool: &Pool<Sqlite>,
id: Uuid,
) -> anyhow::Result<ProjectModel, AppError> {
sqlx::query_as("SELECT * FROM projects WHERE id = $1")
.bind(id)
.fetch_one(pool)
.await
.map_err(|e| match e {
sqlx::Error::RowNotFound => AppError::NotFound,
e => e.into(),
})
}
pub async fn list(pool: &Pool<Sqlite>) -> anyhow::Result<Vec<ProjectModel>> {
sqlx::query_as("SELECT * FROM projects;")
.fetch_all(pool)
.await
.map_err(|e| e.into())
}
pub async fn update(self, pool: &Pool<Sqlite>) -> anyhow::Result<ProjectModel, AppError> {
let now: DateTime<Utc> = Utc::now();
let _ = sqlx::query!(
"UPDATE projects
SET title=$1, color=$2, folder_id=$3, status=$4, sort_order=$5, updated_at=$6, completed_at=$7
WHERE id=$8",
self.title,
self.color,
self.folder_id,
self.status,
self.sort_order,
now,
self.completed_at,
self.id,
)
.execute(pool)
.await?;
ProjectModel::get_by_id(pool, self.id).await
}
pub async fn delete(pool: &Pool<Sqlite>, id: Uuid) -> anyhow::Result<()> {
sqlx::query!("DELETE FROM projects WHERE id = $1", id)
.execute(pool)
.await?;
Ok(())
}
}