1use anyhow::{Result, bail};
2use chrono::{DateTime, Utc};
3
4use crate::db::Database;
5use crate::ids::{TaskId, WorkspaceId};
6use crate::recurrence::RecurrenceSeriesId;
7use crate::refs::DisplayRefContext;
8use crate::types::Task;
9
10mod database_stats;
11mod dependencies;
12mod details;
13mod doctor;
14pub mod fragments;
15mod hydration;
16mod projects;
17mod recent_actions;
18mod recurrence;
19mod search;
20mod sidebar;
21mod sorting;
22mod sync_history;
23mod tasks;
24#[cfg(test)]
25mod test_support;
26mod types;
27
28pub use database_stats::{DatabaseStats, DatabaseStatsPriorityCounts, DatabaseStatsStatusCounts};
29pub use dependencies::{TaskDependencyItem, TaskDependencySummary};
30pub(crate) use dependencies::{task_dependency_summary, task_dependency_summary_with_display_refs};
31pub use details::{TaskDetail, TaskDetailConflict};
32pub(crate) use details::{conflict_display_value, task_detail, task_detail_with_display_refs};
33pub use doctor::WorkspaceTaskCounts;
34pub(crate) use doctor::{unresolved_conflict_count, workspace_task_counts};
35pub(crate) use projects::list_project_items_in_workspace;
36pub(crate) use recent_actions::list_recent_actions_in_workspace;
37pub(crate) use recurrence::task_recurrence_summaries;
38pub use search::{
39 SearchMatchedField, TaskSearchPreviewResultSet, TaskSearchQuery, TaskSearchResult,
40};
41
42pub const MAX_RECURRENCE_HISTORY_LIMIT: usize = 500;
43
44fn validate_search_limit(limit: usize) -> Result<()> {
45 if limit == 0 {
46 bail!("error search-limit-invalid limit=0 min=1 hint=\"pass a limit of 1 or greater\"");
47 }
48 Ok(())
49}
50
51fn validate_recurrence_history_limit(limit: usize) -> Result<()> {
52 if !(1..=MAX_RECURRENCE_HISTORY_LIMIT).contains(&limit) {
53 bail!(
54 "error recurrence-history-limit-invalid limit={} min=1 max={} hint=\"pass a limit between 1 and {}\"",
55 limit,
56 MAX_RECURRENCE_HISTORY_LIMIT,
57 MAX_RECURRENCE_HISTORY_LIMIT
58 );
59 }
60 Ok(())
61}
62pub(crate) use search::{
63 search_task_items_in_workspace, search_task_occurrence_items_in_workspace,
64 search_task_preview_set_in_workspace,
65};
66pub(crate) use sidebar::sidebar_counts_for_scope_in_workspace;
67pub use sync_history::SyncHistoryStats;
68pub(crate) use sync_history::sync_history_stats;
69pub(crate) use tasks::{
70 list_task_items_in_workspace, list_task_items_with_display_refs,
71 list_task_summary_items_in_workspace,
72};
73pub use types::RecentActionTarget;
74pub use types::{
75 AttachmentMetadata, ProjectListItem, RecentActionItem, RecurrenceCounts,
76 RecurrenceHistoryEntry, RecurrenceHistoryKind, RecurrenceHistoryPage, RecurrenceOccurrenceLink,
77 RecurrenceReconciliation, RecurrenceSeriesConflict, RecurrenceSeriesDetail,
78 RecurrenceSeriesLifecycleFilter, RecurrenceSeriesListItem, RecurrenceSeriesListQuery,
79 RecurrenceSeriesSummary, RecurrenceTaskGroup, SidebarCounts, SortDirection,
80 TaskAvailabilityFilter, TaskDependencyLink, TaskFilters, TaskListItem, TaskNote, TaskQueryMode,
81 TaskRecurrenceSummary, TaskSort,
82};
83
84impl Database {
85 pub async fn reconcile_recurrence_reports_at(
90 &self,
91 workspace_id: &WorkspaceId,
92 at: DateTime<Utc>,
93 ) -> Result<RecurrenceReconciliation> {
94 let (workspace, candidates, incomplete) = {
95 let mut conn = self.acquire_reader().await?;
96 let workspace = crate::workspaces::workspace_for_id(&mut conn, workspace_id).await?;
97 let (candidates, incomplete) =
98 recurrence::recurrence_reconciliation_candidates(&mut conn, workspace_id, at)
99 .await?;
100 (workspace, candidates, incomplete)
101 };
102 let mut report = RecurrenceReconciliation {
103 workspace_id: Some(workspace_id.clone()),
104 examined: candidates.len(),
105 incomplete,
106 ..RecurrenceReconciliation::default()
107 };
108 for series_id in candidates {
109 let result = self
110 .reconcile_recurrence_series(&workspace, &series_id, at)
111 .await?;
112 report.changed += usize::from(result.changed);
113 report.lifecycle_blocked += usize::from(result.lifecycle_blocked);
114 }
115 recurrence::validate_reconciliation(&report)?;
116 Ok(report)
117 }
118
119 pub async fn reconcile_recurrence_reports(
120 &self,
121 workspace_id: &WorkspaceId,
122 ) -> Result<RecurrenceReconciliation> {
123 let at = DateTime::parse_from_rfc3339(&crate::ids::now())?.with_timezone(&Utc);
124 self.reconcile_recurrence_reports_at(workspace_id, at).await
125 }
126
127 pub async fn list_recurrence_series_view_at(
128 &self,
129 workspace_id: &WorkspaceId,
130 at: DateTime<Utc>,
131 query: RecurrenceSeriesListQuery,
132 ) -> Result<Vec<RecurrenceSeriesListItem>> {
133 self.reconcile_recurrence_reports_at(workspace_id, at)
134 .await?;
135 self.list_recurrence_series_view_from_current_projection(workspace_id, query)
136 .await
137 }
138
139 #[doc(hidden)]
143 pub async fn list_recurrence_series_view_from_current_projection(
144 &self,
145 workspace_id: &WorkspaceId,
146 query: RecurrenceSeriesListQuery,
147 ) -> Result<Vec<RecurrenceSeriesListItem>> {
148 let mut conn = self.acquire_reader().await?;
149 recurrence::list_recurrence_series_view(&mut conn, workspace_id, &query).await
150 }
151
152 pub async fn list_recurrence_series_view(
153 &self,
154 workspace_id: &WorkspaceId,
155 query: RecurrenceSeriesListQuery,
156 ) -> Result<Vec<RecurrenceSeriesListItem>> {
157 let at = DateTime::parse_from_rfc3339(&crate::ids::now())?.with_timezone(&Utc);
158 self.list_recurrence_series_view_at(workspace_id, at, query)
159 .await
160 }
161
162 pub async fn list_recurrence_series_at(
163 &self,
164 workspace_id: &WorkspaceId,
165 at: DateTime<Utc>,
166 ) -> Result<Vec<RecurrenceSeriesSummary>> {
167 self.reconcile_recurrence_reports_at(workspace_id, at)
168 .await?;
169 let mut conn = self.acquire_reader().await?;
170 recurrence::list_recurrence_series(&mut conn, workspace_id, at).await
171 }
172
173 pub async fn recurrence_series_detail_at(
174 &self,
175 workspace_id: &WorkspaceId,
176 series_id: &RecurrenceSeriesId,
177 at: DateTime<Utc>,
178 ) -> Result<RecurrenceSeriesDetail> {
179 self.reconcile_recurrence_reports_at(workspace_id, at)
180 .await?;
181 self.recurrence_series_detail_at_from_current_projection(workspace_id, series_id, at)
182 .await
183 }
184
185 #[doc(hidden)]
189 pub async fn recurrence_series_detail_at_from_current_projection(
190 &self,
191 workspace_id: &WorkspaceId,
192 series_id: &RecurrenceSeriesId,
193 at: DateTime<Utc>,
194 ) -> Result<RecurrenceSeriesDetail> {
195 let mut conn = self.acquire_reader().await?;
196 recurrence::recurrence_series_detail(&mut conn, workspace_id, series_id, at).await
197 }
198
199 pub async fn recurrence_history_at(
200 &self,
201 workspace_id: &WorkspaceId,
202 series_id: &RecurrenceSeriesId,
203 at: DateTime<Utc>,
204 offset: usize,
205 limit: usize,
206 ) -> Result<RecurrenceHistoryPage> {
207 validate_recurrence_history_limit(limit)?;
208 self.reconcile_recurrence_reports_at(workspace_id, at)
209 .await?;
210 let mut conn = self.acquire_reader().await?;
211 recurrence::recurrence_history(&mut conn, workspace_id, series_id, at, offset, limit).await
212 }
213
214 pub async fn list_recurrence_series(
215 &self,
216 workspace_id: &WorkspaceId,
217 ) -> Result<Vec<RecurrenceSeriesSummary>> {
218 let at = DateTime::parse_from_rfc3339(&crate::ids::now())?.with_timezone(&Utc);
219 self.list_recurrence_series_at(workspace_id, at).await
220 }
221
222 pub async fn recurrence_series_detail(
223 &self,
224 workspace_id: &WorkspaceId,
225 series_id: &RecurrenceSeriesId,
226 ) -> Result<RecurrenceSeriesDetail> {
227 let at = DateTime::parse_from_rfc3339(&crate::ids::now())?.with_timezone(&Utc);
228 self.recurrence_series_detail_at(workspace_id, series_id, at)
229 .await
230 }
231
232 #[doc(hidden)]
236 pub async fn recurrence_series_detail_from_current_projection(
237 &self,
238 workspace_id: &WorkspaceId,
239 series_id: &RecurrenceSeriesId,
240 ) -> Result<RecurrenceSeriesDetail> {
241 let at = DateTime::parse_from_rfc3339(&crate::ids::now())?.with_timezone(&Utc);
242 self.recurrence_series_detail_at_from_current_projection(workspace_id, series_id, at)
243 .await
244 }
245
246 pub async fn recurrence_history(
247 &self,
248 workspace_id: &WorkspaceId,
249 series_id: &RecurrenceSeriesId,
250 offset: usize,
251 limit: usize,
252 ) -> Result<RecurrenceHistoryPage> {
253 let at = DateTime::parse_from_rfc3339(&crate::ids::now())?.with_timezone(&Utc);
254 self.recurrence_history_at(workspace_id, series_id, at, offset, limit)
255 .await
256 }
257
258 pub async fn list_project_items(
259 &self,
260 workspace_id: &WorkspaceId,
261 ) -> Result<Vec<ProjectListItem>> {
262 self.reconcile_recurrence_reports(workspace_id).await?;
263 self.list_project_items_from_current_projection(workspace_id)
264 .await
265 }
266
267 #[doc(hidden)]
271 pub async fn list_project_items_from_current_projection(
272 &self,
273 workspace_id: &WorkspaceId,
274 ) -> Result<Vec<ProjectListItem>> {
275 let mut conn = self.acquire_reader().await?;
276 list_project_items_in_workspace(&mut conn, workspace_id).await
277 }
278
279 pub async fn list_task_items(
280 &self,
281 workspace_id: &WorkspaceId,
282 filters: TaskFilters,
283 mode: TaskQueryMode,
284 sort: TaskSort,
285 direction: SortDirection,
286 ) -> Result<Vec<TaskListItem>> {
287 self.reconcile_recurrence_reports(workspace_id).await?;
288 self.list_task_items_from_current_projection(workspace_id, filters, mode, sort, direction)
289 .await
290 }
291
292 #[doc(hidden)]
296 pub async fn list_task_items_from_current_projection(
297 &self,
298 workspace_id: &WorkspaceId,
299 filters: TaskFilters,
300 mode: TaskQueryMode,
301 sort: TaskSort,
302 direction: SortDirection,
303 ) -> Result<Vec<TaskListItem>> {
304 let mut conn = self.acquire_reader().await?;
305 list_task_items_in_workspace(&mut conn, workspace_id, filters, mode, sort, direction).await
306 }
307
308 pub async fn list_task_summary_items(
309 &self,
310 workspace_id: &WorkspaceId,
311 filters: TaskFilters,
312 mode: TaskQueryMode,
313 sort: TaskSort,
314 direction: SortDirection,
315 limit: Option<usize>,
316 ) -> Result<Vec<TaskListItem>> {
317 self.reconcile_recurrence_reports(workspace_id).await?;
318 let mut conn = self.acquire_reader().await?;
319 list_task_summary_items_in_workspace(
320 &mut conn,
321 workspace_id,
322 filters,
323 mode,
324 sort,
325 direction,
326 limit,
327 )
328 .await
329 }
330
331 pub async fn list_task_items_with_display_refs(
332 &self,
333 workspace_id: &WorkspaceId,
334 filters: TaskFilters,
335 mode: TaskQueryMode,
336 sort: TaskSort,
337 direction: SortDirection,
338 _display_refs: &DisplayRefContext,
339 ) -> Result<Vec<TaskListItem>> {
340 self.reconcile_recurrence_reports(workspace_id).await?;
341 let mut conn = self.acquire_reader().await?;
342 let refreshed_display_refs =
343 DisplayRefContext::for_workspace(&mut conn, workspace_id).await?;
344 list_task_items_with_display_refs(
345 &mut conn,
346 workspace_id,
347 filters,
348 mode,
349 sort,
350 direction,
351 &refreshed_display_refs,
352 )
353 .await
354 }
355
356 pub async fn sidebar_counts_for_scope(
357 &self,
358 workspace_id: &WorkspaceId,
359 project_key: Option<&str>,
360 ) -> Result<SidebarCounts> {
361 self.reconcile_recurrence_reports(workspace_id).await?;
362 self.sidebar_counts_for_scope_from_current_projection(workspace_id, project_key)
363 .await
364 }
365
366 #[doc(hidden)]
370 pub async fn sidebar_counts_for_scope_from_current_projection(
371 &self,
372 workspace_id: &WorkspaceId,
373 project_key: Option<&str>,
374 ) -> Result<SidebarCounts> {
375 let mut conn = self.acquire_reader().await?;
376 sidebar_counts_for_scope_in_workspace(&mut conn, workspace_id, project_key).await
377 }
378
379 pub async fn list_recent_actions(
380 &self,
381 workspace_id: &WorkspaceId,
382 project_scope: Option<&str>,
383 ) -> Result<Vec<RecentActionItem>> {
384 self.reconcile_recurrence_reports(workspace_id).await?;
385 self.list_recent_actions_from_current_projection(workspace_id, project_scope)
386 .await
387 }
388
389 #[doc(hidden)]
393 pub async fn list_recent_actions_from_current_projection(
394 &self,
395 workspace_id: &WorkspaceId,
396 project_scope: Option<&str>,
397 ) -> Result<Vec<RecentActionItem>> {
398 let mut conn = self.acquire_reader().await?;
399 list_recent_actions_in_workspace(&mut conn, workspace_id, project_scope).await
400 }
401
402 pub async fn search_task_items(
403 &self,
404 workspace_id: &WorkspaceId,
405 query: TaskSearchQuery,
406 ) -> Result<Vec<TaskSearchResult>> {
407 validate_search_limit(query.limit)?;
408 self.reconcile_recurrence_reports(workspace_id).await?;
409 let mut conn = self.acquire_reader().await?;
410 search_task_items_in_workspace(&mut conn, workspace_id, query).await
411 }
412
413 pub async fn search_task_occurrence_items(
414 &self,
415 workspace_id: &WorkspaceId,
416 query: TaskSearchQuery,
417 ) -> Result<Vec<TaskSearchResult>> {
418 validate_search_limit(query.limit)?;
419 self.reconcile_recurrence_reports(workspace_id).await?;
420 let mut conn = self.acquire_reader().await?;
421 search_task_occurrence_items_in_workspace(&mut conn, workspace_id, query).await
422 }
423
424 pub async fn search_task_preview_set(
425 &self,
426 workspace_id: &WorkspaceId,
427 query: TaskSearchQuery,
428 ) -> Result<TaskSearchPreviewResultSet> {
429 validate_search_limit(query.limit)?;
430 self.reconcile_recurrence_reports(workspace_id).await?;
431 self.search_task_preview_set_from_current_projection(workspace_id, query)
432 .await
433 }
434
435 #[doc(hidden)]
439 pub async fn search_task_preview_set_from_current_projection(
440 &self,
441 workspace_id: &WorkspaceId,
442 query: TaskSearchQuery,
443 ) -> Result<TaskSearchPreviewResultSet> {
444 validate_search_limit(query.limit)?;
445 let mut conn = self.acquire_reader().await?;
446 search_task_preview_set_in_workspace(&mut conn, workspace_id, query).await
447 }
448
449 pub async fn task_detail(&self, task: &Task) -> Result<TaskDetail> {
450 self.reconcile_recurrence_reports(&task.workspace_id)
451 .await?;
452 let mut conn = self.acquire_reader().await?;
453 task_detail(&mut conn, task).await
454 }
455
456 pub async fn task_detail_with_display_refs(
457 &self,
458 task: &Task,
459 display_refs: &DisplayRefContext,
460 ) -> Result<TaskDetail> {
461 self.reconcile_recurrence_reports(&task.workspace_id)
462 .await?;
463 let mut conn = self.acquire_reader().await?;
464 task_detail_with_display_refs(&mut conn, task, display_refs).await
465 }
466
467 pub async fn conflict_display_value(
468 &self,
469 workspace_id: &WorkspaceId,
470 field: &str,
471 value: &str,
472 ) -> Result<String> {
473 let mut conn = self.acquire_reader().await?;
474 conflict_display_value(&mut conn, workspace_id, field, value).await
475 }
476
477 pub async fn task_dependency_summary(
478 &self,
479 workspace_id: &WorkspaceId,
480 task_id: &TaskId,
481 ) -> Result<TaskDependencySummary> {
482 let mut conn = self.acquire_reader().await?;
483 task_dependency_summary(&mut conn, workspace_id, task_id).await
484 }
485
486 pub async fn workspace_task_counts(
487 &self,
488 workspace_id: &WorkspaceId,
489 ) -> Result<WorkspaceTaskCounts> {
490 let mut conn = self.acquire_reader().await?;
491 workspace_task_counts(&mut conn, workspace_id).await
492 }
493
494 pub async fn unresolved_conflict_count(&self) -> Result<i64> {
495 let mut conn = self.acquire_reader().await?;
496 unresolved_conflict_count(&mut conn).await
497 }
498
499 pub async fn sync_history_stats(&self) -> Result<SyncHistoryStats> {
500 let mut conn = self.acquire_reader().await?;
501 sync_history_stats(&mut conn).await
502 }
503
504 pub async fn database_stats(
505 &self,
506 workspace: &crate::workspaces::Workspace,
507 ) -> Result<DatabaseStats> {
508 let mut conn = self.acquire_reader().await?;
509 database_stats::database_stats(&mut conn, workspace).await
510 }
511}