Create Initial Database Schema for Tasks. (#1)
Create migration scripts for this and initial Task DB Model in rust along with unit tests. Reviewed-on: #1 Co-authored-by: Drew Galbraith <drew@tiramisu.one> Co-committed-by: Drew Galbraith <drew@tiramisu.one>
This commit is contained in:
parent
c2b7c12905
commit
82d524a62f
13 changed files with 1923 additions and 15 deletions
3
backend/src/models/mod.rs
Normal file
3
backend/src/models/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
mod task;
|
||||
|
||||
pub use task::*;
|
||||
269
backend/src/models/task.rs
Normal file
269
backend/src/models/task.rs
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
use anyhow::{Result, bail};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::SqlitePool;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy, sqlx::Type)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[sqlx(rename_all = "lowercase")]
|
||||
pub enum TaskStatus {
|
||||
Todo,
|
||||
Done,
|
||||
Backlog,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
pub struct TaskModel {
|
||||
pub id: Uuid,
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
pub status: TaskStatus,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl TaskModel {
|
||||
pub async fn insert(
|
||||
pool: &SqlitePool,
|
||||
title: &str,
|
||||
description: Option<&str>,
|
||||
) -> Result<TaskModel> {
|
||||
if title.len() == 0 {
|
||||
bail!("Title must not be empty");
|
||||
}
|
||||
|
||||
let id = Uuid::new_v4();
|
||||
let result = sqlx::query_as(
|
||||
"INSERT INTO tasks (id, title, description) VALUES ($1, $2, $3) RETURNING *",
|
||||
)
|
||||
.bind(id)
|
||||
.bind(title)
|
||||
.bind(description)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn get_by_id(pool: &SqlitePool, id: Uuid) -> Result<TaskModel> {
|
||||
let result = sqlx::query_as("SELECT * FROM tasks WHERE id = $1")
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn update(self, pool: &SqlitePool) -> Result<TaskModel> {
|
||||
let now: DateTime<Utc> = Utc::now();
|
||||
|
||||
let _ = sqlx::query(
|
||||
"UPDATE tasks
|
||||
SET title=$1, description=$2, status=$3, updated_at=$4, completed_at=$5
|
||||
WHERE id=$6",
|
||||
)
|
||||
.bind(&self.title)
|
||||
.bind(&self.description)
|
||||
.bind(&self.status)
|
||||
.bind(now)
|
||||
.bind(&self.completed_at)
|
||||
.bind(&self.id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
TaskModel::get_by_id(pool, self.id).await
|
||||
}
|
||||
|
||||
pub async fn delete(pool: &SqlitePool, id: Uuid) -> Result<()> {
|
||||
sqlx::query("DELETE FROM tasks WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_all(pool: &SqlitePool) -> Result<Vec<TaskModel>> {
|
||||
let tasks = sqlx::query_as("SELECT * FROM tasks ORDER BY created_at DESC")
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(tasks)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::database::create_test_pool;
|
||||
|
||||
async fn setup_test_db() -> SqlitePool {
|
||||
create_test_pool()
|
||||
.await
|
||||
.expect("Failed to create test pool")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_insert_task() {
|
||||
let pool = setup_test_db().await;
|
||||
|
||||
let title = "Test Task";
|
||||
let description = "Test Description";
|
||||
|
||||
let task = TaskModel::insert(&pool, title, Some(description))
|
||||
.await
|
||||
.expect("Failed to insert task");
|
||||
|
||||
assert_eq!(task.title, title);
|
||||
assert_eq!(task.description, Some(description.to_string()));
|
||||
assert_eq!(task.status, TaskStatus::Todo);
|
||||
assert!(task.completed_at.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_insert_task_with_null_description() {
|
||||
let pool = setup_test_db().await;
|
||||
|
||||
let title = "Test Task";
|
||||
let description = None;
|
||||
|
||||
let task = TaskModel::insert(&pool, title, description)
|
||||
.await
|
||||
.expect("Failed to insert task");
|
||||
|
||||
assert_eq!(task.title, title);
|
||||
assert_eq!(task.description, None);
|
||||
assert_eq!(task.status, TaskStatus::Todo);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_by_id_found() {
|
||||
let pool = setup_test_db().await;
|
||||
|
||||
let title = "Test Task";
|
||||
let description = Some("Test Description");
|
||||
|
||||
let inserted_task = TaskModel::insert(&pool, title, description)
|
||||
.await
|
||||
.expect("Failed to insert task");
|
||||
|
||||
let retrieved_task = TaskModel::get_by_id(&pool, inserted_task.id)
|
||||
.await
|
||||
.expect("Failed to get task by id");
|
||||
|
||||
assert_eq!(retrieved_task.id, inserted_task.id);
|
||||
assert_eq!(retrieved_task.title, inserted_task.title);
|
||||
assert_eq!(retrieved_task.description, inserted_task.description);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_by_id_not_found() {
|
||||
let pool = setup_test_db().await;
|
||||
|
||||
let non_existent_id = Uuid::new_v4();
|
||||
let result = TaskModel::get_by_id(&pool, non_existent_id).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_task() {
|
||||
let pool = setup_test_db().await;
|
||||
|
||||
let title = "Original Task";
|
||||
let description = Some("Original Description");
|
||||
|
||||
let mut task = TaskModel::insert(&pool, title, description)
|
||||
.await
|
||||
.expect("Failed to insert task");
|
||||
|
||||
// Update the task
|
||||
task.title = "Updated Task".to_string();
|
||||
task.description = Some("Updated Description".to_string());
|
||||
task.status = TaskStatus::Done;
|
||||
task.completed_at = Some(Utc::now());
|
||||
|
||||
let updated_task = task.update(&pool).await.expect("Failed to update task");
|
||||
|
||||
assert_eq!(updated_task.title, "Updated Task");
|
||||
assert_eq!(
|
||||
updated_task.description,
|
||||
Some("Updated Description".to_string())
|
||||
);
|
||||
assert_eq!(updated_task.status, TaskStatus::Done);
|
||||
assert!(updated_task.completed_at.is_some());
|
||||
assert!(updated_task.updated_at > updated_task.created_at);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_task() {
|
||||
let pool = setup_test_db().await;
|
||||
|
||||
let title = "Task to Delete";
|
||||
let description = Some("This will be deleted");
|
||||
|
||||
let task = TaskModel::insert(&pool, title, description)
|
||||
.await
|
||||
.expect("Failed to insert task");
|
||||
|
||||
// Delete the task
|
||||
TaskModel::delete(&pool, task.id)
|
||||
.await
|
||||
.expect("Failed to delete task");
|
||||
|
||||
// Verify it's gone
|
||||
let result = TaskModel::get_by_id(&pool, task.id).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_all_tasks() {
|
||||
let pool = setup_test_db().await;
|
||||
|
||||
// Insert multiple tasks
|
||||
let _task1 = TaskModel::insert(&pool, "Task 1", Some("Desc 1"))
|
||||
.await
|
||||
.expect("Failed to insert task 1");
|
||||
|
||||
let _task2 = TaskModel::insert(&pool, "Task 2", None)
|
||||
.await
|
||||
.expect("Failed to insert task 2");
|
||||
|
||||
let tasks = TaskModel::list_all(&pool)
|
||||
.await
|
||||
.expect("Failed to list tasks");
|
||||
|
||||
assert_eq!(tasks.len(), 2);
|
||||
|
||||
// Check that both tasks are present (order may vary due to timing)
|
||||
let titles: Vec<&String> = tasks.iter().map(|t| &t.title).collect();
|
||||
assert!(titles.contains(&&"Task 1".to_string()));
|
||||
assert!(titles.contains(&&"Task 2".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_task_status_serialization() {
|
||||
let pool = setup_test_db().await;
|
||||
|
||||
// Test each status
|
||||
let statuses = vec![TaskStatus::Todo, TaskStatus::Done, TaskStatus::Backlog];
|
||||
|
||||
for status in statuses {
|
||||
let mut task = TaskModel::insert(&pool, "Test Task", None)
|
||||
.await
|
||||
.expect("Failed to insert task");
|
||||
|
||||
task.status = status;
|
||||
task = task.update(&pool).await.expect("Failed to update task");
|
||||
|
||||
assert_eq!(task.status, status);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_empty_title_constraint() {
|
||||
let pool = setup_test_db().await;
|
||||
|
||||
let result = TaskModel::insert(&pool, "", None).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue