Skip to main content

qefro_backend_sdk/
storage.rs

1//! Managed document storage via storage-service (ADR-002). Used only from app tools.
2
3use anyhow::{anyhow, Result};
4use serde_json::{json, Value};
5
6use crate::customer_hub::PlatformCapabilities;
7
8#[derive(Clone, Default)]
9pub struct StorageContext {
10    platform: Option<PlatformCapabilities>,
11}
12
13impl StorageContext {
14    pub fn new(platform: Option<PlatformCapabilities>) -> Self {
15        Self { platform }
16    }
17
18    fn endpoint(&self) -> Result<(String, String, Value)> {
19        let storage = self.platform.as_ref().and_then(|p| p.storage.as_ref());
20        let from_env = std::env::var("QEFRO_STORAGE_URL")
21            .ok()
22            .map(|s| s.trim_end_matches('/').to_string());
23        let base_url = storage
24            .and_then(|s| s.base_url.clone())
25            .or(from_env)
26            .unwrap_or_default()
27            .trim_end_matches('/')
28            .to_string();
29        if base_url.is_empty() {
30            return Err(anyhow!(
31                "ctx.storage requires platform.storage.base_url or QEFRO_STORAGE_URL"
32            ));
33        }
34        let context = storage
35            .and_then(|s| s.context.clone())
36            .ok_or_else(|| anyhow!("ctx.storage requires platform.storage.context on tool.invoke"))?;
37        let token = storage
38            .and_then(|s| s.token.clone())
39            .or_else(|| std::env::var("QEFRO_SERVICE_TOKEN").ok())
40            .or_else(|| std::env::var("QEFRO_INTERNAL_TOKEN").ok())
41            .unwrap_or_default();
42        Ok((base_url, token, serde_json::to_value(context)?))
43    }
44
45    async fn call(&self, op: &str, mut body: Value) -> Result<Value> {
46        let (base_url, token, context) = self.endpoint()?;
47        if let Some(obj) = body.as_object_mut() {
48            obj.insert("context".into(), context);
49        }
50        let mut req = reqwest::Client::new()
51            .post(format!("{base_url}/v1/internal/storage/{op}"))
52            .header("content-type", "application/json")
53            .json(&body);
54        if !token.is_empty() {
55            req = req.header("authorization", format!("Bearer {token}"));
56        }
57        let res = req.send().await?;
58        let status = res.status();
59        let text = res.text().await.unwrap_or_default();
60        if !status.is_success() {
61            return Err(anyhow!("storage.{op} failed ({status}): {text}"));
62        }
63        if text.is_empty() {
64            return Ok(json!({}));
65        }
66        Ok(serde_json::from_str(&text).unwrap_or_else(|_| json!({})))
67    }
68
69    pub async fn insert(
70        &self,
71        collection: &str,
72        document: Value,
73        allocate_code: Option<Value>,
74    ) -> Result<Value> {
75        let mut body = json!({ "collection": collection, "document": document });
76        if let Some(code) = allocate_code {
77            body["allocate_code"] = code;
78        }
79        self.call("insert", body).await
80    }
81
82    pub async fn find(
83        &self,
84        collection: &str,
85        filter: Option<Value>,
86        limit: Option<u64>,
87        sort: Option<Value>,
88    ) -> Result<Value> {
89        let mut body = json!({
90            "collection": collection,
91            "filter": filter.unwrap_or_else(|| json!({})),
92        });
93        if let Some(limit) = limit {
94            body["limit"] = json!(limit);
95        }
96        if let Some(sort) = sort {
97            body["sort"] = sort;
98        }
99        let out = self.call("find", body).await?;
100        let items = out
101            .get("items")
102            .and_then(|v| v.as_array())
103            .cloned()
104            .unwrap_or_default();
105        let total = out
106            .get("total")
107            .and_then(|v| v.as_u64())
108            .unwrap_or(items.len() as u64);
109        Ok(json!({ "items": items, "total": total }))
110    }
111
112    pub async fn get(&self, collection: &str, id: &str) -> Result<Value> {
113        self.call("get", json!({ "collection": collection, "id": id }))
114            .await
115    }
116
117    pub async fn update(&self, collection: &str, id: &str, patch: Value) -> Result<Value> {
118        self.call(
119            "update",
120            json!({ "collection": collection, "id": id, "patch": patch }),
121        )
122        .await
123    }
124
125    pub async fn delete(&self, collection: &str, id: &str) -> Result<Value> {
126        self.call("delete", json!({ "collection": collection, "id": id }))
127            .await
128    }
129}
130
131pub fn build_storage_context(platform: Option<PlatformCapabilities>) -> StorageContext {
132    StorageContext::new(platform)
133}