Skip to main content

things3_core/database/queries/
projects.rs

1use crate::{
2    database::{conversions::safe_timestamp_convert, mappers::map_project_row, ThingsDatabase},
3    error::{Result as ThingsResult, ThingsError},
4    models::{Project, TaskStatus, ThingsId},
5};
6use chrono::{DateTime, Utc};
7use sqlx::Row;
8use tracing::{debug, instrument};
9
10impl ThingsDatabase {
11    /// Get all projects (from `TMTask` table where type = 1)
12    ///
13    /// # Errors
14    ///
15    /// Returns an error if the database query fails or if project data is invalid
16    #[instrument]
17    pub async fn get_all_projects(&self) -> ThingsResult<Vec<Project>> {
18        let rows = sqlx::query(
19            r"
20            SELECT 
21                uuid, title, status, 
22                area, notes, 
23                creationDate, userModificationDate,
24                startDate, deadline
25            FROM TMTask
26            WHERE type = 1 AND trashed = 0
27            ORDER BY creationDate DESC
28            ",
29        )
30        .fetch_all(&self.pool)
31        .await
32        .map_err(|e| ThingsError::unknown(format!("Failed to fetch projects: {e}")))?;
33
34        let mut projects = Vec::new();
35        for row in rows {
36            let project = Project {
37                uuid: ThingsId::from_trusted(row.get::<String, _>("uuid")),
38                title: row.get("title"),
39                status: TaskStatus::from_i32(row.get("status")).unwrap_or(TaskStatus::Incomplete),
40                area_uuid: row
41                    .get::<Option<String>, _>("area")
42                    .map(ThingsId::from_trusted),
43                notes: row.get("notes"),
44                deadline: row
45                    .get::<Option<i64>, _>("deadline")
46                    .and_then(|ts| DateTime::from_timestamp(ts, 0))
47                    .map(|dt| dt.date_naive()),
48                start_date: row
49                    .get::<Option<i64>, _>("startDate")
50                    .and_then(|ts| DateTime::from_timestamp(ts, 0))
51                    .map(|dt| dt.date_naive()),
52                tags: Vec::new(),  // TODO: Load tags separately
53                tasks: Vec::new(), // TODO: Load child tasks separately
54                created: {
55                    let ts_f64 = row.get::<f64, _>("creationDate");
56                    let ts = safe_timestamp_convert(ts_f64);
57                    DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
58                },
59                modified: {
60                    let ts_f64 = row.get::<f64, _>("userModificationDate");
61                    let ts = safe_timestamp_convert(ts_f64);
62                    DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
63                },
64            };
65            projects.push(project);
66        }
67
68        debug!("Fetched {} projects", projects.len());
69        Ok(projects)
70    }
71
72    /// Get all projects (alias for `get_all_projects` for compatibility)
73    ///
74    /// # Errors
75    ///
76    /// Returns an error if the database query fails or if project data is invalid
77    #[instrument(skip(self))]
78    pub async fn get_projects(&self, limit: Option<usize>) -> ThingsResult<Vec<Project>> {
79        let _ = limit; // Currently unused but kept for API compatibility
80        self.get_all_projects().await
81    }
82
83    /// Get a single project by UUID
84    ///
85    /// Returns `None` if the project doesn't exist or is trashed
86    ///
87    /// # Errors
88    ///
89    /// Returns an error if the database query fails
90    #[instrument(skip(self))]
91    pub async fn get_project_by_uuid(&self, id: &ThingsId) -> ThingsResult<Option<Project>> {
92        let row = sqlx::query(
93            r"
94            SELECT
95                uuid, title, status,
96                area, notes,
97                creationDate, userModificationDate,
98                startDate, deadline,
99                trashed, type
100            FROM TMTask
101            WHERE uuid = ? AND type = 1
102            ",
103        )
104        .bind(id.as_str())
105        .fetch_optional(&self.pool)
106        .await
107        .map_err(|e| ThingsError::unknown(format!("Failed to fetch project: {e}")))?;
108
109        if let Some(row) = row {
110            let trashed: i64 = row.get("trashed");
111            if trashed == 1 {
112                return Ok(None);
113            }
114            Ok(Some(map_project_row(&row)))
115        } else {
116            Ok(None)
117        }
118    }
119}