Skip to main content

macro_factor_api/
firestore.rs

1use anyhow::{anyhow, Result};
2use reqwest::Client;
3use serde::Deserialize;
4use serde_json::{json, Map, Value};
5
6use crate::auth::{FirebaseAuth, PROJECT_ID};
7
8const BASE_URL: &str = "https://firestore.googleapis.com/v1";
9
10#[derive(Clone)]
11pub struct FirestoreClient {
12    client: Client,
13    auth: FirebaseAuth,
14}
15
16#[derive(Debug, Deserialize)]
17pub struct Document {
18    pub name: String,
19    pub fields: Option<Map<String, Value>>,
20    #[serde(rename = "createTime")]
21    pub create_time: Option<String>,
22    #[serde(rename = "updateTime")]
23    pub update_time: Option<String>,
24}
25
26#[derive(Debug, Deserialize)]
27struct ListDocumentsResponse {
28    documents: Option<Vec<Document>>,
29    #[serde(rename = "nextPageToken")]
30    next_page_token: Option<String>,
31}
32
33#[derive(Debug, Deserialize)]
34struct RunQueryResponse {
35    document: Option<Document>,
36    #[allow(dead_code)]
37    #[serde(rename = "readTime")]
38    read_time: Option<String>,
39}
40
41#[derive(Debug, Deserialize)]
42struct ListCollectionIdsResponse {
43    #[serde(rename = "collectionIds")]
44    collection_ids: Option<Vec<String>>,
45    #[serde(rename = "nextPageToken")]
46    next_page_token: Option<String>,
47}
48
49impl FirestoreClient {
50    pub fn new(auth: FirebaseAuth) -> Self {
51        Self {
52            client: Client::new(),
53            auth,
54        }
55    }
56
57    fn documents_base(&self) -> String {
58        format!(
59            "{}/projects/{}/databases/(default)/documents",
60            BASE_URL, PROJECT_ID
61        )
62    }
63
64    pub async fn get_document(&self, path: &str) -> Result<Document> {
65        let token = self.auth.get_id_token().await?;
66        let url = format!("{}/{}", self.documents_base(), path);
67
68        let resp = self.client.get(&url).bearer_auth(&token).send().await?;
69
70        if !resp.status().is_success() {
71            let status = resp.status();
72            let body = resp.text().await.unwrap_or_default();
73            return Err(anyhow!("GET {} failed: {} - {}", path, status, body));
74        }
75
76        Ok(resp.json().await?)
77    }
78
79    pub async fn list_documents(
80        &self,
81        collection_path: &str,
82        page_size: Option<u32>,
83        page_token: Option<&str>,
84    ) -> Result<(Vec<Document>, Option<String>)> {
85        let token = self.auth.get_id_token().await?;
86        let url = format!("{}/{}", self.documents_base(), collection_path);
87
88        let mut req = self.client.get(&url).bearer_auth(&token);
89
90        if let Some(size) = page_size {
91            req = req.query(&[("pageSize", size.to_string())]);
92        }
93        if let Some(pt) = page_token {
94            req = req.query(&[("pageToken", pt)]);
95        }
96
97        let resp = req.send().await?;
98
99        if !resp.status().is_success() {
100            let status = resp.status();
101            let body = resp.text().await.unwrap_or_default();
102            return Err(anyhow!(
103                "LIST {} failed: {} - {}",
104                collection_path,
105                status,
106                body
107            ));
108        }
109
110        let list_resp: ListDocumentsResponse = resp.json().await?;
111        Ok((
112            list_resp.documents.unwrap_or_default(),
113            list_resp.next_page_token,
114        ))
115    }
116
117    pub async fn list_collection_ids(&self, parent_path: Option<&str>) -> Result<Vec<String>> {
118        let token = self.auth.get_id_token().await?;
119        let parent = match parent_path {
120            Some(p) => format!("{}/{}", self.documents_base(), p),
121            None => self.documents_base(),
122        };
123        let url = format!("{}:listCollectionIds", parent);
124
125        let mut all_ids = Vec::new();
126        let mut page_token: Option<String> = None;
127
128        loop {
129            let mut body = json!({});
130            if let Some(ref pt) = page_token {
131                body["pageToken"] = json!(pt);
132            }
133
134            let resp = self
135                .client
136                .post(&url)
137                .bearer_auth(&token)
138                .json(&body)
139                .send()
140                .await?;
141
142            if !resp.status().is_success() {
143                let status = resp.status();
144                let body = resp.text().await.unwrap_or_default();
145                return Err(anyhow!("listCollectionIds failed: {} - {}", status, body));
146            }
147
148            let list_resp: ListCollectionIdsResponse = resp.json().await?;
149            if let Some(ids) = list_resp.collection_ids {
150                all_ids.extend(ids);
151            }
152
153            match list_resp.next_page_token {
154                Some(pt) if !pt.is_empty() => page_token = Some(pt),
155                _ => break,
156            }
157        }
158
159        Ok(all_ids)
160    }
161
162    pub async fn run_query(
163        &self,
164        parent_path: Option<&str>,
165        structured_query: Value,
166    ) -> Result<Vec<Document>> {
167        let token = self.auth.get_id_token().await?;
168        let parent = match parent_path {
169            Some(p) => format!("{}/{}", self.documents_base(), p),
170            None => self.documents_base(),
171        };
172        let url = format!("{}:runQuery", parent);
173
174        let body = json!({
175            "structuredQuery": structured_query
176        });
177
178        let resp = self
179            .client
180            .post(&url)
181            .bearer_auth(&token)
182            .json(&body)
183            .send()
184            .await?;
185
186        if !resp.status().is_success() {
187            let status = resp.status();
188            let body = resp.text().await.unwrap_or_default();
189            return Err(anyhow!("runQuery failed: {} - {}", status, body));
190        }
191
192        let results: Vec<RunQueryResponse> = resp.json().await?;
193        Ok(results.into_iter().filter_map(|r| r.document).collect())
194    }
195
196    /// Update (PATCH) specific fields in a document.
197    /// Creates the document if it doesn't exist.
198    pub async fn patch_document(
199        &self,
200        path: &str,
201        fields: Map<String, Value>,
202        field_paths: &[&str],
203    ) -> Result<Document> {
204        let token = self.auth.get_id_token().await?;
205        let url = format!("{}/{}", self.documents_base(), path);
206
207        let mut req = self.client.patch(&url).bearer_auth(&token);
208
209        for fp in field_paths {
210            req = req.query(&[("updateMask.fieldPaths", *fp)]);
211        }
212
213        let body = json!({
214            "fields": fields
215        });
216
217        let resp: reqwest::Response = req.json(&body).send().await?;
218
219        if !resp.status().is_success() {
220            let status = resp.status();
221            let text = resp.text().await.unwrap_or_default();
222            return Err(anyhow!("PATCH {} failed: {} - {}", path, status, text));
223        }
224
225        Ok(resp.json().await?)
226    }
227}
228
229/// Convert a serde_json::Value into Firestore's typed value format.
230pub fn to_firestore_value(val: &Value) -> Value {
231    match val {
232        Value::Null => json!({"nullValue": null}),
233        Value::Bool(b) => json!({"booleanValue": b}),
234        Value::Number(n) => {
235            if let Some(i) = n.as_i64() {
236                json!({"integerValue": i.to_string()})
237            } else if let Some(f) = n.as_f64() {
238                json!({"doubleValue": f})
239            } else {
240                json!({"integerValue": n.to_string()})
241            }
242        }
243        Value::String(s) => json!({"stringValue": s}),
244        Value::Array(arr) => {
245            let values: Vec<Value> = arr.iter().map(to_firestore_value).collect();
246            json!({"arrayValue": {"values": values}})
247        }
248        Value::Object(map) => {
249            let mut fields = Map::new();
250            for (k, v) in map {
251                fields.insert(k.clone(), to_firestore_value(v));
252            }
253            json!({"mapValue": {"fields": fields}})
254        }
255    }
256}
257
258/// Convert a flat JSON object into Firestore fields format.
259pub fn to_firestore_fields(obj: &Value) -> Map<String, Value> {
260    let mut fields = Map::new();
261    if let Some(map) = obj.as_object() {
262        for (k, v) in map {
263            fields.insert(k.clone(), to_firestore_value(v));
264        }
265    }
266    fields
267}
268
269/// Parse a Firestore typed value into a serde_json::Value.
270pub fn parse_firestore_value(val: &Value) -> Value {
271    if let Some(s) = val.get("stringValue") {
272        return s.clone();
273    }
274    if let Some(i) = val.get("integerValue") {
275        // Firestore sends integers as strings
276        if let Some(s) = i.as_str() {
277            if let Ok(n) = s.parse::<i64>() {
278                return json!(n);
279            }
280        }
281        return i.clone();
282    }
283    if let Some(d) = val.get("doubleValue") {
284        return d.clone();
285    }
286    if let Some(b) = val.get("booleanValue") {
287        return b.clone();
288    }
289    if val.get("nullValue").is_some() {
290        return Value::Null;
291    }
292    if let Some(ts) = val.get("timestampValue") {
293        return ts.clone();
294    }
295    if let Some(r) = val.get("referenceValue") {
296        return r.clone();
297    }
298    if let Some(geo) = val.get("geoPointValue") {
299        return geo.clone();
300    }
301    if let Some(bytes) = val.get("bytesValue") {
302        return bytes.clone();
303    }
304    if let Some(map) = val.get("mapValue") {
305        if let Some(fields) = map.get("fields") {
306            return parse_firestore_fields(fields);
307        }
308        return json!({});
309    }
310    if let Some(arr) = val.get("arrayValue") {
311        if let Some(values) = arr.get("values").and_then(|v| v.as_array()) {
312            return Value::Array(values.iter().map(parse_firestore_value).collect());
313        }
314        return json!([]);
315    }
316
317    // Unknown format, return as-is
318    val.clone()
319}
320
321/// Parse Firestore document fields into a flat JSON object.
322pub fn parse_firestore_fields(fields: &Value) -> Value {
323    if let Some(map) = fields.as_object() {
324        let mut result = Map::new();
325        for (key, val) in map {
326            result.insert(key.clone(), parse_firestore_value(val));
327        }
328        Value::Object(result)
329    } else {
330        Value::Null
331    }
332}
333
334/// Parse a full Firestore document into a JSON object with parsed fields.
335pub fn parse_document(doc: &Document) -> Value {
336    let mut result = Map::new();
337
338    // Extract document ID from the name path
339    let name = &doc.name;
340    if let Some(id) = name.rsplit('/').next() {
341        result.insert("_id".to_string(), json!(id));
342    }
343    result.insert("_path".to_string(), json!(name));
344
345    if let Some(ref fields) = doc.fields {
346        if let Value::Object(parsed) = parse_firestore_fields(&Value::Object(fields.clone())) {
347            for (key, val) in parsed {
348                result.insert(key, val);
349            }
350        }
351    }
352
353    Value::Object(result)
354}