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 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
229pub 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
258pub 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
269pub 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 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 val.clone()
319}
320
321pub 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
334pub fn parse_document(doc: &Document) -> Value {
336 let mut result = Map::new();
337
338 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}