sova-devtools 0.1.10

In-app DevTools bar for Sova (HTML inject, SSE timeline, request snapshots)
Documentation
//! AppStore / KvStore console actions.

use super::{json_response, ActionResponse, MAX_BODY};
use crate::console::DevToolsConsole;
use bytes::Bytes;
use serde::Deserialize;
use serde_json::json;
use sova_core::{Request, Response};
use sova_store::AppStore;
use std::time::{Duration, Instant};

#[derive(Debug, Deserialize)]
pub struct StoreActionRequest {
    pub namespace: String,
    pub op: String,
    #[serde(default)]
    pub key: Option<String>,
    #[serde(default)]
    pub value: Option<String>,
    #[serde(default)]
    pub ttl_secs: Option<u64>,
    #[serde(default)]
    pub by: Option<i64>,
    #[serde(default)]
    pub prefix: Option<String>,
}

pub async fn handle(req: &mut Request, cfg: &DevToolsConsole) -> Response {
    let started = Instant::now();
    let body: StoreActionRequest = match req.json().await {
        Ok(b) => b,
        Err(e) => {
            return json_response(ActionResponse::err(format!("invalid JSON: {e}"), 0.0), 400);
        }
    };

    if body.namespace.trim().is_empty() {
        return json_response(ActionResponse::err("namespace required", 0.0), 400);
    }

    let store = match req.try_state::<AppStore>() {
        Some(s) => s,
        None => {
            return json_response(ActionResponse::err("AppStore not installed", 0.0), 503);
        }
    };

    let ns = store.namespaced(body.namespace.trim());
    let ms = || started.elapsed().as_secs_f64() * 1000.0;

    match body.op.to_ascii_lowercase().as_str() {
        "get" => {
            let Some(key) = body.key.filter(|k| !k.is_empty()) else {
                return json_response(ActionResponse::err("key required", ms()), 400);
            };
            let val = ns.get(&key).await;
            let text = val
                .as_ref()
                .map(|b| String::from_utf8_lossy(b).into_owned())
                .unwrap_or_default();
            json_response(
                ActionResponse::ok(
                    json!({ "value": text, "bytes": val.map(|b| b.len()) }),
                    ms(),
                ),
                200,
            )
        }
        "set" => {
            let key = match body.key.filter(|k| !k.is_empty()) {
                Some(k) => k,
                None => return json_response(ActionResponse::err("key required", ms()), 400),
            };
            let value = body.value.unwrap_or_default();
            if value.len() > cfg.body_limit.min(MAX_BODY) {
                return json_response(ActionResponse::err("value too large", ms()), 400);
            }
            let ttl = body.ttl_secs.map(Duration::from_secs);
            ns.set(&key, Bytes::from(value), ttl).await;
            json_response(ActionResponse::ok(json!({ "ok": true }), ms()), 200)
        }
        "del" => {
            let key = match body.key.filter(|k| !k.is_empty()) {
                Some(k) => k,
                None => return json_response(ActionResponse::err("key required", ms()), 400),
            };
            ns.remove(&key).await;
            json_response(ActionResponse::ok(json!({ "ok": true }), ms()), 200)
        }
        "incr" => {
            let key = match body.key.filter(|k| !k.is_empty()) {
                Some(k) => k,
                None => return json_response(ActionResponse::err("key required", ms()), 400),
            };
            let by = body.by.unwrap_or(1);
            let ttl = body.ttl_secs.map(Duration::from_secs);
            let n = ns.incr(&key, by, ttl).await;
            json_response(ActionResponse::ok(json!({ "value": n }), ms()), 200)
        }
        "clear_prefix" => {
            let prefix = body.prefix.unwrap_or_default();
            if prefix.trim().is_empty() {
                return json_response(ActionResponse::err("prefix required (safety)", ms()), 403);
            }
            let n = ns.clear_prefix(prefix.trim()).await;
            json_response(ActionResponse::ok(json!({ "removed": n }), ms()), 200)
        }
        other => json_response(
            ActionResponse::err(format!("unknown op `{other}`"), ms()),
            400,
        ),
    }
}