//! `GET /admin/changes[/]`: who wrote what, newest first, of the whole document //! or of what is at a path (as `/store/` names it). Every change names the node that //! committed it (its author) and, for a write through the API, the user who wrote it (in //! its message, `User:`), with when (its writer's clock) and the store paths it touched //! (`document::replica::Writer`). Every node holds every change, so any node answers //! for the whole swarm, as far as it has synced. use axum::{ Json, extract::{Path, Query, State}, http::StatusCode, }; use serde::{Deserialize, Serialize}; use crate::document::store::{self, InLists}; use crate::state::AppState; /// Changes listed at most. const MAX_LIMIT: usize = 1000; /// Changes looked at at most, newest first, for those touching `path`: each costs /// decoding it, under the document's read lock (`store::changed_by`). const MAX_SCANNED: usize = 10_000; #[derive(Debug, Deserialize)] pub(crate) struct ChangesQuery { /// How many, at most (100 by default, up to `MAX_LIMIT`). limit: Option, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ChangeReport { hash: String, /// When, by its writer's clock (Unix milliseconds); 0 for a remake's snapshot. at: i64, /// The node that made it (its node ID), if it says. #[serde(skip_serializing_if = "Option::is_none")] node: Option, /// The user it was written for through the API, if any. #[serde(skip_serializing_if = "Option::is_none")] user: Option, /// The store paths it touched (a list's items by index). paths: Vec, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ChangesReport { changes: Vec, /// Older ones weren't looked at (`MAX_SCANNED`, or `limit` reached). more: bool, } /// `GET /admin/changes`: all of them. pub(crate) async fn get_changes( State(state): State, Query(query): Query, ) -> Result, StatusCode> { report(state, query, None).await } /// `GET /admin/changes/{key}`: those that touched what is at the path, inside it, or /// above it. pub(crate) async fn get_changes_at( State(state): State, Path(key): Path, Query(query): Query, ) -> Result, StatusCode> { let segments: Vec = key.split('/').map(str::to_owned).collect(); if segments.iter().any(String::is_empty) { return Err(StatusCode::BAD_REQUEST); } report(state, query, Some(segments)).await } async fn report( state: AppState, query: ChangesQuery, path: Option>, ) -> Result, StatusCode> { if !state.is_ready().await { return Err(StatusCode::SERVICE_UNAVAILABLE); } let limit = query.limit.unwrap_or(100).clamp(1, MAX_LIMIT); Ok(Json( state .render( move |doc| changes(doc, limit, path.as_deref()), |report| report, ) .await, )) } fn changes( doc: &crate::document::Document, limit: usize, path: Option<&[String]>, ) -> ChangesReport { let all = doc.get_changes_meta(&[]); let mut changes = Vec::new(); let mut scanned = 0; let mut more = false; for meta in all.iter().rev() { if changes.len() == limit || scanned == MAX_SCANNED { more = true; break; } scanned += 1; // What this change did, from its operations. let paths = store::changed_by(doc, &meta.hash, InLists::Index).unwrap_or_default(); if let Some(path) = path && !paths .iter() .any(|touched| touched.starts_with(path) || path.starts_with(touched)) { continue; } changes.push(ChangeReport { hash: meta.hash.to_string(), at: meta.timestamp, node: meta .author .as_ref() .and_then(|author| String::from_utf8(author.as_bytes().to_vec()).ok()), user: meta .message .as_deref() .and_then(crate::document::replica::user_of) .map(str::to_owned), paths: paths.iter().map(|path| path.join("/")).collect(), }); } ChangesReport { changes, more } }