aven-core 0.1.18

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

use crate::sync::wire::ChangeWire;

use super::shared::str_payload;
use super::shared::workspace_id_payload;

pub(super) async fn create_label(conn: &mut SqliteConnection, change: &ChangeWire) -> Result<()> {
    let workspace_id = workspace_id_payload(conn, change).await?;
    let name = str_payload(&change.payload, "name")?;
    let created_at =
        str_payload(&change.payload, "created_at").unwrap_or_else(|_| crate::ids::now());
    insert_label(conn, &workspace_id, &name, &created_at).await
}

pub(super) async fn add_label(conn: &mut SqliteConnection, change: &ChangeWire) -> Result<()> {
    let workspace_id = workspace_id_payload(conn, change).await?;
    let task_id = super::shared::task_id(change)?;
    let label = str_payload(&change.payload, "label")?;
    insert_label(conn, &workspace_id, &label, &change.created_at).await?;
    sqlx::query("INSERT OR IGNORE INTO task_labels(workspace_id, task_id, label) VALUES (?, ?, ?)")
        .bind(&workspace_id)
        .bind(&task_id)
        .bind(&label)
        .execute(&mut *conn)
        .await?;
    Ok(())
}

pub(super) async fn set_label_name(conn: &mut SqliteConnection, change: &ChangeWire) -> Result<()> {
    let workspace_id = workspace_id_payload(conn, change).await?;
    let name = str_payload(&change.payload, "name")?;
    let new_name = str_payload(&change.payload, "new_name")?;
    let created_at: Option<String> =
        sqlx::query_scalar("SELECT created_at FROM labels WHERE workspace_id = ? AND name = ?")
            .bind(&workspace_id)
            .bind(&name)
            .fetch_optional(&mut *conn)
            .await?;
    let Some(created_at) = created_at else {
        return Ok(());
    };
    insert_label(conn, &workspace_id, &new_name, &created_at).await?;
    sqlx::query(
        "INSERT OR IGNORE INTO task_labels(workspace_id, task_id, label)
         SELECT workspace_id, task_id, ? FROM task_labels WHERE workspace_id = ? AND label = ?",
    )
    .bind(&new_name)
    .bind(&workspace_id)
    .bind(&name)
    .execute(&mut *conn)
    .await?;
    sqlx::query(
        "INSERT OR IGNORE INTO recurrence_series_labels(workspace_id, series_id, label)
         SELECT workspace_id, series_id, ? FROM recurrence_series_labels WHERE workspace_id = ? AND label = ?",
    )
    .bind(&new_name)
    .bind(&workspace_id)
    .bind(&name)
    .execute(&mut *conn)
    .await?;
    delete_label_rows(conn, &workspace_id, &name).await
}

pub(super) async fn restore_label(conn: &mut SqliteConnection, change: &ChangeWire) -> Result<()> {
    let workspace_id = workspace_id_payload(conn, change).await?;
    let name = str_payload(&change.payload, "name")?;
    let created_at = str_payload(&change.payload, "created_at")?;
    insert_label(conn, &workspace_id, &name, &created_at).await?;
    for task_id in string_array_payload(&change.payload, "task_ids")? {
        sqlx::query(
            "INSERT OR IGNORE INTO task_labels(workspace_id, task_id, label)
             SELECT ?, id, ? FROM tasks WHERE workspace_id = ? AND id = ?",
        )
        .bind(&workspace_id)
        .bind(&name)
        .bind(&workspace_id)
        .bind(task_id)
        .execute(&mut *conn)
        .await?;
    }
    for series_id in string_array_payload(&change.payload, "series_ids")? {
        sqlx::query(
            "INSERT OR IGNORE INTO recurrence_series_labels(workspace_id, series_id, label)
             SELECT ?, id, ? FROM recurrence_series WHERE workspace_id = ? AND id = ?",
        )
        .bind(&workspace_id)
        .bind(&name)
        .bind(&workspace_id)
        .bind(series_id)
        .execute(&mut *conn)
        .await?;
    }
    Ok(())
}

pub(super) async fn delete_label(conn: &mut SqliteConnection, change: &ChangeWire) -> Result<()> {
    let workspace_id = workspace_id_payload(conn, change).await?;
    let name = str_payload(&change.payload, "name")?;
    delete_label_rows(conn, &workspace_id, &name).await
}

async fn delete_label_rows(
    conn: &mut SqliteConnection,
    workspace_id: &WorkspaceId,
    name: &str,
) -> Result<()> {
    sqlx::query("DELETE FROM task_labels WHERE workspace_id = ? AND label = ?")
        .bind(workspace_id)
        .bind(name)
        .execute(&mut *conn)
        .await?;
    sqlx::query("DELETE FROM recurrence_series_labels WHERE workspace_id = ? AND label = ?")
        .bind(workspace_id)
        .bind(name)
        .execute(&mut *conn)
        .await?;
    sqlx::query("DELETE FROM labels WHERE workspace_id = ? AND name = ?")
        .bind(workspace_id)
        .bind(name)
        .execute(&mut *conn)
        .await?;
    Ok(())
}

pub(super) async fn remove_label(conn: &mut SqliteConnection, change: &ChangeWire) -> Result<()> {
    let workspace_id = workspace_id_payload(conn, change).await?;
    let task_id = super::shared::task_id(change)?;
    let label = str_payload(&change.payload, "label")?;
    sqlx::query("DELETE FROM task_labels WHERE workspace_id = ? AND task_id = ? AND label = ?")
        .bind(&workspace_id)
        .bind(&task_id)
        .bind(&label)
        .execute(&mut *conn)
        .await?;
    Ok(())
}

pub(super) async fn create_or_update_task_label(
    conn: &mut SqliteConnection,
    workspace_id: &WorkspaceId,
    task_id: &crate::ids::TaskId,
    label: &str,
    created_at: &str,
) -> Result<()> {
    insert_label(conn, workspace_id, label, created_at).await?;
    sqlx::query("INSERT OR IGNORE INTO task_labels(workspace_id, task_id, label) VALUES (?, ?, ?)")
        .bind(workspace_id)
        .bind(task_id)
        .bind(label)
        .execute(&mut *conn)
        .await?;
    Ok(())
}

fn string_array_payload(payload: &Value, key: &str) -> Result<Vec<String>> {
    payload
        .get(key)
        .and_then(Value::as_array)
        .context("payload missing label references")?
        .iter()
        .map(|value| {
            value
                .as_str()
                .map(str::to_string)
                .context("invalid label reference")
        })
        .collect()
}

async fn insert_label(
    conn: &mut SqliteConnection,
    workspace_id: &WorkspaceId,
    name: &str,
    created_at: &str,
) -> Result<()> {
    sqlx::query("INSERT OR IGNORE INTO labels(workspace_id, name, created_at) VALUES (?, ?, ?)")
        .bind(workspace_id)
        .bind(name)
        .bind(created_at)
        .execute(&mut *conn)
        .await?;
    Ok(())
}