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