Skip to main content

things3_core/database/
validators.rs

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