qefro_backend_sdk/
storage.rs1use 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}