haematite-cli 0.21.0

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
//! Per-document deletion entries keep the single-delete attribution shape.

use std::collections::BTreeSet;

use serde::{Deserialize, Serialize};

use super::super::identifier;
use crate::serve::protocol::DocumentDeleteMany;
use crate::serve::storage::Failure;

#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "kind", deny_unknown_fields)]
pub enum Deletion {
    #[serde(rename = "document.delete")]
    Document {
        collection: String,
        document_id: String,
    },
}

pub(super) fn entries(delete: &DocumentDeleteMany) -> Vec<Deletion> {
    delete
        .document_ids
        .iter()
        .map(|id| Deletion::Document {
            collection: delete.collection.clone(),
            document_id: id.clone(),
        })
        .collect()
}

pub(super) fn validate(entries: &[Deletion]) -> Result<(), Failure> {
    if entries.is_empty() {
        return Err(Failure::new(
            "change_record_count",
            "an accepted delete batch cannot be empty",
        ));
    }
    let mut identities = BTreeSet::new();
    let mut collection = None;
    for Deletion::Document {
        collection: name,
        document_id,
    } in entries
    {
        identifier(name)?;
        identifier(document_id)?;
        if collection.is_some_and(|previous| previous != name) {
            return Err(Failure::new(
                "change_record_collection",
                "a delete batch names one collection",
            ));
        }
        collection = Some(name);
        if !identities.insert(document_id) {
            return Err(Failure::new(
                "change_record_duplicate",
                "duplicate deletion attribution",
            ));
        }
    }
    Ok(())
}