things3_core/database/mutations/
tasks.rs1use crate::{
2 database::{
3 conversions::naive_date_to_things_timestamp, query_builders::TaskUpdateBuilder, validators,
4 ThingsDatabase,
5 },
6 error::{Result as ThingsResult, ThingsError},
7 models::{
8 CreateTaskRequest, DeleteChildHandling, TaskStatus, TaskType, ThingsId, UpdateTaskRequest,
9 },
10};
11use chrono::Utc;
12use sqlx::Row;
13use tracing::{info, instrument};
14
15impl ThingsDatabase {
16 #[instrument(skip(self))]
60 pub async fn create_task(&self, request: CreateTaskRequest) -> ThingsResult<ThingsId> {
61 crate::database::validate_date_range(request.start_date, request.deadline)?;
63
64 let id = ThingsId::new_things_native();
66
67 if let Some(project_uuid) = &request.project_uuid {
69 validators::validate_project_exists(&self.pool, project_uuid).await?;
70 }
71
72 if let Some(area_uuid) = &request.area_uuid {
73 validators::validate_area_exists(&self.pool, area_uuid).await?;
74 }
75
76 if let Some(parent_uuid) = &request.parent_uuid {
77 validators::validate_task_exists(&self.pool, parent_uuid).await?;
78 }
79
80 let start_date_ts = request.start_date.map(naive_date_to_things_timestamp);
82 let deadline_ts = request.deadline.map(naive_date_to_things_timestamp);
83
84 let now = Utc::now().timestamp() as f64;
86
87 sqlx::query(
89 r"
90 INSERT INTO TMTask (
91 uuid, title, type, status, notes,
92 startDate, deadline, project, area, heading,
93 creationDate, userModificationDate,
94 trashed
95 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
96 ",
97 )
98 .bind(id.as_str())
99 .bind(&request.title)
100 .bind(request.task_type.unwrap_or(TaskType::Todo) as i32)
101 .bind(request.status.unwrap_or(TaskStatus::Incomplete) as i32)
102 .bind(request.notes.as_ref())
103 .bind(start_date_ts)
104 .bind(deadline_ts)
105 .bind(request.project_uuid.map(|u| u.into_string()))
106 .bind(request.area_uuid.map(|u| u.into_string()))
107 .bind(request.parent_uuid.map(|u| u.into_string()))
108 .bind(now)
109 .bind(now)
110 .bind(0) .execute(&self.pool)
112 .await
113 .map_err(|e| ThingsError::unknown(format!("Failed to create task: {e}")))?;
114
115 if let Some(tags) = request.tags {
117 self.set_task_tags(&id, tags).await?;
118 }
119
120 info!("Created task with UUID: {}", id);
121 Ok(id)
122 }
123
124 #[instrument(skip(self))]
133 pub async fn update_task(&self, request: UpdateTaskRequest) -> ThingsResult<()> {
134 validators::validate_task_exists(&self.pool, &request.uuid).await?;
136
137 if request.start_date.is_some() || request.deadline.is_some() {
139 if let Some(current_task) = self.get_task_by_uuid(&request.uuid).await? {
141 let final_start = request.start_date.or(current_task.start_date);
142 let final_deadline = request.deadline.or(current_task.deadline);
143 crate::database::validate_date_range(final_start, final_deadline)?;
144 }
145 }
146
147 if let Some(project_uuid) = &request.project_uuid {
149 validators::validate_project_exists(&self.pool, project_uuid).await?;
150 }
151
152 if let Some(area_uuid) = &request.area_uuid {
153 validators::validate_area_exists(&self.pool, area_uuid).await?;
154 }
155
156 let builder = TaskUpdateBuilder::from_request(&request);
158
159 if builder.is_empty() {
161 return Ok(());
162 }
163
164 let query_string = builder.build_query_string();
165 let mut q = sqlx::query(&query_string);
166
167 if let Some(title) = &request.title {
169 q = q.bind(title);
170 }
171
172 if let Some(notes) = &request.notes {
173 q = q.bind(notes);
174 }
175
176 if let Some(start_date) = request.start_date {
177 q = q.bind(naive_date_to_things_timestamp(start_date));
178 }
179
180 if let Some(deadline) = request.deadline {
181 q = q.bind(naive_date_to_things_timestamp(deadline));
182 }
183
184 if let Some(status) = request.status {
185 q = q.bind(status as i32);
186 }
187
188 if let Some(project_uuid) = request.project_uuid {
189 q = q.bind(project_uuid.into_string());
190 }
191
192 if let Some(area_uuid) = request.area_uuid {
193 q = q.bind(area_uuid.into_string());
194 }
195
196 let now = Utc::now().timestamp() as f64;
198 q = q.bind(now).bind(request.uuid.as_str());
199
200 q.execute(&self.pool)
201 .await
202 .map_err(|e| ThingsError::unknown(format!("Failed to update task: {e}")))?;
203
204 if let Some(tags) = request.tags {
206 self.set_task_tags(&request.uuid, tags).await?;
207 }
208
209 info!("Updated task with UUID: {}", request.uuid);
210 Ok(())
211 }
212
213 #[instrument(skip(self))]
219 pub async fn complete_task(&self, id: &ThingsId) -> ThingsResult<()> {
220 validators::validate_task_exists(&self.pool, id).await?;
222
223 let now = Utc::now().timestamp() as f64;
224
225 sqlx::query(
226 "UPDATE TMTask SET status = 3, stopDate = ?, userModificationDate = ? WHERE uuid = ?",
227 )
228 .bind(now)
229 .bind(now)
230 .bind(id.as_str())
231 .execute(&self.pool)
232 .await
233 .map_err(|e| ThingsError::unknown(format!("Failed to complete task: {e}")))?;
234
235 info!("Completed task with UUID: {}", id);
236 Ok(())
237 }
238
239 #[instrument(skip(self))]
245 pub async fn uncomplete_task(&self, id: &ThingsId) -> ThingsResult<()> {
246 validators::validate_task_exists(&self.pool, id).await?;
248
249 let now = Utc::now().timestamp() as f64;
250
251 sqlx::query(
252 "UPDATE TMTask SET status = 0, stopDate = NULL, userModificationDate = ? WHERE uuid = ?",
253 )
254 .bind(now)
255 .bind(id.as_str())
256 .execute(&self.pool)
257 .await
258 .map_err(|e| ThingsError::unknown(format!("Failed to uncomplete task: {e}")))?;
259
260 info!("Uncompleted task with UUID: {}", id);
261 Ok(())
262 }
263
264 #[instrument(skip(self))]
270 pub async fn delete_task(
271 &self,
272 id: &ThingsId,
273 child_handling: DeleteChildHandling,
274 ) -> ThingsResult<()> {
275 validators::validate_task_exists(&self.pool, id).await?;
277
278 let children = sqlx::query("SELECT uuid FROM TMTask WHERE heading = ? AND trashed = 0")
280 .bind(id.as_str())
281 .fetch_all(&self.pool)
282 .await
283 .map_err(|e| ThingsError::unknown(format!("Failed to query child tasks: {e}")))?;
284
285 let has_children = !children.is_empty();
286
287 if has_children {
288 match child_handling {
289 DeleteChildHandling::Error => {
290 return Err(ThingsError::unknown(format!(
291 "Task {} has {} child task(s). Use cascade or orphan mode to delete.",
292 id,
293 children.len()
294 )));
295 }
296 DeleteChildHandling::Cascade => {
297 let now = Utc::now().timestamp() as f64;
299 for child_row in &children {
300 let child_uuid: String = child_row.get("uuid");
301 sqlx::query(
302 "UPDATE TMTask SET trashed = 1, userModificationDate = ? WHERE uuid = ?",
303 )
304 .bind(now)
305 .bind(&child_uuid)
306 .execute(&self.pool)
307 .await
308 .map_err(|e| {
309 ThingsError::unknown(format!("Failed to delete child task: {e}"))
310 })?;
311 }
312 info!("Cascade deleted {} child task(s)", children.len());
313 }
314 DeleteChildHandling::Orphan => {
315 let now = Utc::now().timestamp() as f64;
317 for child_row in &children {
318 let child_uuid: String = child_row.get("uuid");
319 sqlx::query(
320 "UPDATE TMTask SET heading = NULL, userModificationDate = ? WHERE uuid = ?",
321 )
322 .bind(now)
323 .bind(&child_uuid)
324 .execute(&self.pool)
325 .await
326 .map_err(|e| {
327 ThingsError::unknown(format!("Failed to orphan child task: {e}"))
328 })?;
329 }
330 info!("Orphaned {} child task(s)", children.len());
331 }
332 }
333 }
334
335 let now = Utc::now().timestamp() as f64;
337 sqlx::query("UPDATE TMTask SET trashed = 1, userModificationDate = ? WHERE uuid = ?")
338 .bind(now)
339 .bind(id.as_str())
340 .execute(&self.pool)
341 .await
342 .map_err(|e| ThingsError::unknown(format!("Failed to delete task: {e}")))?;
343
344 info!("Deleted task with UUID: {}", id);
345 Ok(())
346 }
347}