aven-core 0.1.12

Core library for the Aven local-first task manager
use anyhow::{Context, Result, ensure};
use serde_json::Value;
use sqlx::SqliteConnection;

use crate::choices::{TaskPriority, TaskStatus};
use crate::db::{field_version, set_field_version};
use crate::mutation::{apply_field_value_in_workspace, apply_project_id_in_workspace};
use crate::sync::wire::ChangeWire;
use crate::task_fields::TaskField;

use super::conflict;
use super::label::create_or_update_task_label;
use super::payload::CreateTaskPayload;
use super::project::ensure_project_for_payload;
use super::shared::{str_payload, task_field_workspace_id_payload, task_id, workspace_id_payload};

pub(super) async fn create_task(conn: &mut SqliteConnection, change: &ChangeWire) -> Result<()> {
    let p = CreateTaskPayload::from_change(change)?;
    let task_id = task_id(change)?;
    let workspace_id = workspace_id_payload(conn, change).await?;
    if sqlx::query_scalar::<_, i64>("SELECT count(*) FROM tasks WHERE workspace_id = ? AND id = ?")
        .bind(&workspace_id)
        .bind(&task_id)
        .fetch_one(&mut *conn)
        .await?
        > 0
    {
        return Ok(());
    }
    let project_id = ensure_project_for_payload(conn, &workspace_id, &p.project_id, change).await?;
    let title = p.title;
    let description = p.description.unwrap_or_default();
    let status = match p.status {
        Some(ref value) => TaskStatus::parse(value)?,
        None => TaskStatus::Inbox,
    };
    let priority = match p.priority {
        Some(ref value) => TaskPriority::parse(value)?,
        None => TaskPriority::None,
    };
    let available_at = p.available_at.unwrap_or_default();
    crate::time_validation::validate_available_at_value(&available_at)?;
    let due_on = p.due_on.unwrap_or_default();
    crate::time_validation::validate_due_on_value(&due_on)?;
    let is_epic = match p.is_epic.as_deref() {
        Some("1") | Some("true") => 1,
        _ => 0,
    };
    let created_at = p.created_at.unwrap_or_else(|| change.created_at.clone());
    sqlx::query(
        "INSERT INTO tasks(workspace_id, id, title, description, project_id, status, priority, created_at, updated_at, queue_activity_at, available_at, due_on, is_epic)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
    )
    .bind(&workspace_id)
    .bind(&task_id)
    .bind(&title)
    .bind(&description)
    .bind(&project_id)
    .bind(status.as_str())
    .bind(priority.as_str())
    .bind(&created_at)
    .bind(&change.created_at)
    .bind(&change.created_at)
    .bind(&available_at)
    .bind(&due_on)
    .bind(is_epic)
    .execute(&mut *conn)
    .await?;
    if let Some(labels) = change.payload.get("labels").and_then(Value::as_array) {
        for label in labels.iter().filter_map(Value::as_str) {
            create_or_update_task_label(conn, &workspace_id, &task_id, label, &change.created_at)
                .await?;
        }
    }
    for field in TaskField::VERSIONED {
        set_field_version(conn, &task_id, field.as_str(), &change.change_id).await?;
    }
    Ok(())
}

pub async fn set_field(
    conn: &mut SqliteConnection,
    change: &ChangeWire,
    force: bool,
) -> Result<()> {
    let task_id = task_id(change)?;
    let field = change
        .field
        .as_deref()
        .context("field change missing field")?;
    let task_field = TaskField::parse_or_unknown(field)?;
    let field = task_field.as_str();
    let mut value = str_payload(&change.payload, "value")?;
    let workspace_id = task_field_workspace_id_payload(conn, change).await?;
    let mut resolved_project_id = None;
    if task_field.is_project() {
        let project_id = str_payload(&change.payload, "project_id")?;
        ensure!(
            value == project_id,
            "error invalid-sync-change project-value-mismatch"
        );
        let project_id = project_id.parse()?;
        let local_project_id =
            ensure_project_for_payload(conn, &workspace_id, &project_id, change).await?;
        value = local_project_id.to_string();
        resolved_project_id = Some(local_project_id);
    }
    if !force {
        let current = field_version(conn, &task_id, field).await?;
        if task_field == TaskField::IsEpic
            && value == "0"
            && crate::operations::task_has_epic_children(conn, &workspace_id, &task_id).await?
        {
            conflict::create_conflict(
                conn,
                change,
                &workspace_id,
                field,
                &value,
                current.as_deref(),
            )
            .await?;
            return Ok(());
        }
        if current != change.base_version {
            conflict::create_conflict(
                conn,
                change,
                &workspace_id,
                field,
                &value,
                current.as_deref(),
            )
            .await?;
            return Ok(());
        }
    }
    if force
        && task_field == TaskField::IsEpic
        && value == "0"
        && crate::operations::task_has_epic_children(conn, &workspace_id, &task_id).await?
    {
        anyhow::bail!("error epic-has-children task_id={}", change.entity_id);
    }
    if task_field.is_project() {
        apply_project_id_in_workspace(
            conn,
            &workspace_id,
            &task_id,
            resolved_project_id
                .as_ref()
                .context("project field missing resolved project ID")?,
        )
        .await?;
    } else {
        apply_field_value_in_workspace(conn, &workspace_id, &task_id, field, &value).await?;
    }
    set_field_version(conn, &task_id, field, &change.change_id).await?;
    if force {
        sqlx::query(
            "UPDATE conflicts SET resolved = 1 WHERE workspace_id = ? AND task_id = ? AND field = ? AND resolved = 0",
        )
        .bind(&workspace_id)
        .bind(&task_id)
        .bind(field)
        .execute(&mut *conn)
        .await?;
    }
    Ok(())
}