things3_core/database/
validators.rs

1//! Entity validation utilities for database operations
2//!
3//! This module provides centralized validation functions to ensure
4//! referenced entities (tasks, projects, areas) exist before performing operations.
5
6use crate::error::{Result as ThingsResult, ThingsError};
7use sqlx::SqlitePool;
8use tracing::instrument;
9use uuid::Uuid;
10
11/// Validate that a task exists and is not trashed
12///
13/// # Errors
14///
15/// Returns an error if the task does not exist, is trashed, or if the database query fails
16#[instrument(skip(pool))]
17pub async fn validate_task_exists(pool: &SqlitePool, uuid: &Uuid) -> ThingsResult<()> {
18    let exists = sqlx::query("SELECT 1 FROM TMTask WHERE uuid = ? AND trashed = 0")
19        .bind(uuid.to_string())
20        .fetch_optional(pool)
21        .await
22        .map_err(|e| ThingsError::unknown(format!("Failed to validate task: {e}")))?
23        .is_some();
24
25    if !exists {
26        return Err(ThingsError::unknown(format!("Task not found: {uuid}")));
27    }
28    Ok(())
29}
30
31/// Validate that a project exists (project is a task with type = 1)
32///
33/// # Errors
34///
35/// Returns an error if the project does not exist, is trashed, or if the database query fails
36#[instrument(skip(pool))]
37pub async fn validate_project_exists(pool: &SqlitePool, uuid: &Uuid) -> ThingsResult<()> {
38    let exists = sqlx::query("SELECT 1 FROM TMTask WHERE uuid = ? AND type = 1 AND trashed = 0")
39        .bind(uuid.to_string())
40        .fetch_optional(pool)
41        .await
42        .map_err(|e| ThingsError::unknown(format!("Failed to validate project: {e}")))?
43        .is_some();
44
45    if !exists {
46        return Err(ThingsError::ProjectNotFound {
47            uuid: uuid.to_string(),
48        });
49    }
50    Ok(())
51}
52
53/// Validate that an area exists
54///
55/// # Errors
56///
57/// Returns an error if the area does not exist or if the database query fails
58#[instrument(skip(pool))]
59pub async fn validate_area_exists(pool: &SqlitePool, uuid: &Uuid) -> ThingsResult<()> {
60    let exists = sqlx::query("SELECT 1 FROM TMArea WHERE uuid = ?")
61        .bind(uuid.to_string())
62        .fetch_optional(pool)
63        .await
64        .map_err(|e| ThingsError::unknown(format!("Failed to validate area: {e}")))?
65        .is_some();
66
67    if !exists {
68        return Err(ThingsError::unknown(format!("Area not found: {uuid}")));
69    }
70    Ok(())
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[cfg(feature = "test-utils")]
78    #[tokio::test]
79    async fn test_validate_nonexistent_task() {
80        use crate::test_utils::create_test_database;
81        use tempfile::NamedTempFile;
82
83        let temp_file = NamedTempFile::new().unwrap();
84        let db_path = temp_file.path();
85        create_test_database(db_path).await.unwrap();
86
87        let pool = sqlx::SqlitePool::connect(&format!("sqlite://{}", db_path.display()))
88            .await
89            .unwrap();
90
91        let nonexistent_uuid = Uuid::new_v4();
92        let result = validate_task_exists(&pool, &nonexistent_uuid).await;
93
94        assert!(result.is_err());
95        assert!(result.unwrap_err().to_string().contains("Task not found"));
96    }
97
98    #[cfg(feature = "test-utils")]
99    #[tokio::test]
100    async fn test_validate_nonexistent_project() {
101        use crate::test_utils::create_test_database;
102        use tempfile::NamedTempFile;
103
104        let temp_file = NamedTempFile::new().unwrap();
105        let db_path = temp_file.path();
106        create_test_database(db_path).await.unwrap();
107
108        let pool = sqlx::SqlitePool::connect(&format!("sqlite://{}", db_path.display()))
109            .await
110            .unwrap();
111
112        let nonexistent_uuid = Uuid::new_v4();
113        let result = validate_project_exists(&pool, &nonexistent_uuid).await;
114
115        assert!(result.is_err());
116        assert!(result
117            .unwrap_err()
118            .to_string()
119            .contains("Project not found"));
120    }
121
122    #[cfg(feature = "test-utils")]
123    #[tokio::test]
124    async fn test_validate_nonexistent_area() {
125        use crate::test_utils::create_test_database;
126        use tempfile::NamedTempFile;
127
128        let temp_file = NamedTempFile::new().unwrap();
129        let db_path = temp_file.path();
130        create_test_database(db_path).await.unwrap();
131
132        let pool = sqlx::SqlitePool::connect(&format!("sqlite://{}", db_path.display()))
133            .await
134            .unwrap();
135
136        let nonexistent_uuid = Uuid::new_v4();
137        let result = validate_area_exists(&pool, &nonexistent_uuid).await;
138
139        assert!(result.is_err());
140        assert!(result.unwrap_err().to_string().contains("Area not found"));
141    }
142}