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