Skip to main content

aven_core/query/
dependencies.rs

1use crate::ids::WorkspaceId;
2use anyhow::Result;
3use sqlx::{Row, SqliteConnection};
4
5use crate::choices::TaskStatus;
6use crate::refs::DisplayRefContext;
7use crate::types::Task;
8
9use super::fragments;
10
11#[derive(Debug)]
12pub struct TaskDependencyItem {
13    pub task: Task,
14    pub display_ref: String,
15    pub created_at: String,
16    pub unresolved: bool,
17}
18
19#[derive(Debug)]
20pub struct TaskDependencySummary {
21    pub depends_on: Vec<TaskDependencyItem>,
22    pub blocks: Vec<TaskDependencyItem>,
23}
24
25pub async fn task_dependency_summary(
26    conn: &mut SqliteConnection,
27    workspace_id: &WorkspaceId,
28    task_id: &crate::ids::TaskId,
29) -> Result<TaskDependencySummary> {
30    let display_refs = DisplayRefContext::for_workspace(conn, workspace_id).await?;
31    task_dependency_summary_with_display_refs(conn, workspace_id, task_id, &display_refs).await
32}
33
34pub async fn task_dependency_summary_with_display_refs(
35    conn: &mut SqliteConnection,
36    workspace_id: &WorkspaceId,
37    task_id: &crate::ids::TaskId,
38    display_refs: &DisplayRefContext,
39) -> Result<TaskDependencySummary> {
40    let depends_on = query_dependency_items(&mut *conn, workspace_id, task_id, false, display_refs)
41        .await?
42        .into_iter()
43        .collect::<Vec<_>>();
44    let blocks = query_dependency_items(&mut *conn, workspace_id, task_id, true, display_refs)
45        .await?
46        .into_iter()
47        .collect::<Vec<_>>();
48    Ok(TaskDependencySummary { depends_on, blocks })
49}
50
51async fn query_dependency_items(
52    conn: &mut SqliteConnection,
53    workspace_id: &WorkspaceId,
54    task_id: &crate::ids::TaskId,
55    blocks_only: bool,
56    display_refs: &DisplayRefContext,
57) -> Result<Vec<TaskDependencyItem>> {
58    let rows = if blocks_only {
59        sqlx::query(
60            "SELECT t.id, t.workspace_id, t.title, t.description, t.project_id,
61         p.key AS project_key, p.prefix AS project_prefix, t.status, t.priority, t.created_at, t.updated_at,
62         t.queue_activity_at, t.available_at, t.due_on, t.deleted, t.is_epic, d.created_at AS dependency_created_at
63         FROM task_dependencies d
64         JOIN tasks t ON t.workspace_id = d.workspace_id AND t.id = d.task_id
65         JOIN projects p ON p.workspace_id = t.workspace_id AND p.id = t.project_id
66         WHERE d.workspace_id = ? AND d.depends_on_task_id = ?",
67        )
68        .bind(workspace_id)
69        .bind(task_id)
70        .fetch_all(&mut *conn)
71        .await?
72    } else {
73        sqlx::query(
74            "SELECT t.id, t.workspace_id, t.title, t.description, t.project_id,
75         p.key AS project_key, p.prefix AS project_prefix, t.status, t.priority, t.created_at, t.updated_at,
76         t.queue_activity_at, t.available_at, t.due_on, t.deleted, t.is_epic, d.created_at AS dependency_created_at
77         FROM task_dependencies d
78         JOIN tasks t ON t.workspace_id = d.workspace_id AND t.id = d.depends_on_task_id
79         JOIN projects p ON p.workspace_id = t.workspace_id AND p.id = t.project_id
80         WHERE d.workspace_id = ? AND d.task_id = ?",
81        )
82        .bind(workspace_id)
83        .bind(task_id)
84        .fetch_all(&mut *conn)
85        .await?
86    };
87
88    let subject_is_open = if blocks_only {
89        subject_task_is_open(conn, workspace_id, task_id).await?
90    } else {
91        true
92    };
93    let mut rows_tasks = rows
94        .iter()
95        .map(crate::db::task_from_row)
96        .collect::<Result<Vec<_>>>()?;
97    let mut items = rows
98        .into_iter()
99        .zip(rows_tasks.drain(..))
100        .map(|(row, task)| {
101            let created_at: String = row.get("dependency_created_at");
102            let task_is_open = !task.deleted && task.status.is_open();
103            let unresolved = task_is_open && (!blocks_only || subject_is_open);
104            let display_ref = display_refs.display_ref(&task);
105            TaskDependencyItem {
106                task,
107                display_ref,
108                created_at,
109                unresolved,
110            }
111        })
112        .collect::<Vec<_>>();
113
114    items.sort_by(|a, b| {
115        b.unresolved.cmp(&a.unresolved).then_with(|| {
116            status_order(a.task.status)
117                .cmp(&status_order(b.task.status))
118                .then_with(|| a.task.title.cmp(&b.task.title))
119                .then_with(|| a.created_at.cmp(&b.created_at))
120                .then_with(|| a.task.id.cmp(&b.task.id))
121        })
122    });
123    Ok(items)
124}
125
126async fn subject_task_is_open(
127    conn: &mut SqliteConnection,
128    workspace_id: &WorkspaceId,
129    task_id: &crate::ids::TaskId,
130) -> Result<bool> {
131    let sql = format!(
132        "SELECT count(*) FROM tasks
133         WHERE workspace_id = ? AND id = ? AND {}",
134        fragments::open_task_clause("tasks"),
135    );
136    let open: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(sql.as_str()))
137        .bind(workspace_id)
138        .bind(task_id)
139        .fetch_one(&mut *conn)
140        .await?;
141    Ok(open > 0)
142}
143
144fn status_order(status: TaskStatus) -> u8 {
145    match status {
146        TaskStatus::Active => 0,
147        TaskStatus::Todo => 1,
148        TaskStatus::Inbox => 2,
149        TaskStatus::Backlog => 3,
150        TaskStatus::Done => 4,
151        TaskStatus::Canceled => 5,
152    }
153}