aven-core 0.1.32

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

use crate::error::CoreError;
use crate::ids::{new_id, now};

use super::{get_meta, set_meta};

async fn next_local_seq(conn: &mut SqliteConnection) -> Result<i64> {
    let seq = get_meta(conn, "local_seq")
        .await?
        .unwrap_or_else(|| "0".to_string())
        .parse::<i64>()?
        + 1;
    set_meta(conn, "local_seq", &seq.to_string()).await?;
    Ok(seq)
}

async fn serialize_local_change(
    conn: &mut SqliteConnection,
    op_type: &str,
    field: Option<&str>,
    payload: &Value,
) -> Result<String> {
    let protocol = crate::sync::protocol::replica_protocol(conn).await?;
    crate::sync::protocol::validate_operation(protocol, op_type, field, payload)?;
    crate::sync::wire::serialize_change_payload(payload)
}

pub(crate) async fn insert_change(
    conn: &mut SqliteConnection,
    entity_type: &str,
    entity_id: &str,
    field: Option<&str>,
    op_type: &str,
    payload: Value,
    base_version: Option<&str>,
) -> Result<String> {
    let payload = serialize_local_change(conn, op_type, field, &payload).await?;
    let change_id = new_id();
    let client_id = get_meta(conn, "client_id")
        .await?
        .context("missing client id")?;
    let local_seq = next_local_seq(conn).await?;
    let created_at = now();
    sqlx::query!(
        "INSERT INTO changes(change_id, client_id, local_seq, entity_type, entity_id, field,
         op_type, payload, base_version, created_at)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
        change_id,
        client_id,
        local_seq,
        entity_type,
        entity_id,
        field,
        op_type,
        payload,
        base_version,
        created_at,
    )
    .execute(&mut *conn)
    .await?;
    Ok(change_id)
}

pub(crate) struct IdentifiedChange<'a> {
    pub change_id: &'a str,
    pub entity_type: &'a str,
    pub entity_id: &'a str,
    pub field: Option<&'a str>,
    pub op_type: &'a str,
    pub payload: Value,
    pub base_version: Option<&'a str>,
    pub created_at: &'a str,
}

pub(crate) async fn insert_change_with_identity(
    conn: &mut SqliteConnection,
    change: IdentifiedChange<'_>,
) -> Result<()> {
    let IdentifiedChange {
        change_id,
        entity_type,
        entity_id,
        field,
        op_type,
        payload,
        base_version,
        created_at,
    } = change;
    let payload = serialize_local_change(conn, op_type, field, &payload).await?;
    let existing = sqlx::query(
        "SELECT entity_type, entity_id, field, op_type, payload, base_version, created_at
         FROM changes WHERE change_id = ?",
    )
    .bind(change_id)
    .fetch_optional(&mut *conn)
    .await?;
    if let Some(existing) = existing {
        let equal = existing.try_get::<String, _>("entity_type")? == entity_type
            && existing.try_get::<String, _>("entity_id")? == entity_id
            && existing.try_get::<Option<String>, _>("field")?.as_deref() == field
            && existing.try_get::<String, _>("op_type")? == op_type
            && existing.try_get::<String, _>("payload")? == payload
            && existing
                .try_get::<Option<String>, _>("base_version")?
                .as_deref()
                == base_version
            && existing.try_get::<String, _>("created_at")? == created_at;
        if equal {
            return Ok(());
        }
        return Err(CoreError::generation_conflict(format!(
            "error recurrence-generation-conflict change_id={change_id}"
        ))
        .into());
    }

    let client_id = get_meta(conn, "client_id")
        .await?
        .context("missing client id")?;
    let local_seq = next_local_seq(conn).await?;
    sqlx::query(
        "INSERT INTO changes(change_id, client_id, local_seq, entity_type, entity_id, field,
         op_type, payload, base_version, created_at)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
    )
    .bind(change_id)
    .bind(client_id)
    .bind(local_seq)
    .bind(entity_type)
    .bind(entity_id)
    .bind(field)
    .bind(op_type)
    .bind(payload)
    .bind(base_version)
    .bind(created_at)
    .execute(&mut *conn)
    .await?;
    Ok(())
}