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 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
244pub 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
273pub 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
284pub 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 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 val.clone()
334}
335
336pub 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
349pub fn parse_document(doc: &Document) -> Value {
351 let mut result = Map::new();
352
353 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}