firebase_admin_sdk/firestore/
batch.rs

1use super::models::{
2    CommitRequest, CommitResponse, Document, DocumentMask, Precondition, Write, WriteOperation,
3    WriteResult,
4};
5use super::reference::convert_serializable_to_fields;
6use super::FirestoreError;
7use reqwest::header;
8use reqwest_middleware::ClientWithMiddleware;
9use serde::Serialize;
10use std::sync::{Arc, Mutex};
11
12/// Represents a Firestore Write Batch.
13///
14/// specific set of writes can be performed atomically.
15///
16/// # Examples
17///
18/// ```rust,ignore
19/// let batch = firestore.batch();
20/// batch.set("users/user1", &user1)?;
21/// batch.update("users/user2", &user2_updates)?;
22/// batch.delete("users/user3")?;
23/// batch.commit().await?;
24/// ```
25#[derive(Clone)]
26pub struct WriteBatch<'a> {
27    client: &'a ClientWithMiddleware,
28    base_url: String,
29    writes: Arc<Mutex<Vec<Write>>>,
30}
31
32impl<'a> WriteBatch<'a> {
33    pub(crate) fn new(client: &'a ClientWithMiddleware, base_url: String) -> Self {
34        Self {
35            client,
36            base_url,
37            writes: Arc::new(Mutex::new(Vec::new())),
38        }
39    }
40
41    /// Overwrites the document referred to by `document_path`.
42    ///
43    /// If the document does not exist, it will be created. If it does exist, it will be overwritten.
44    ///
45    /// # Arguments
46    ///
47    /// * `document_path` - The path to the document to write.
48    /// * `value` - The data to write.
49    pub fn set<T: Serialize>(
50        &self,
51        document_path: &str,
52        value: &T,
53    ) -> Result<&Self, FirestoreError> {
54        let fields = convert_serializable_to_fields(value)?;
55        let resource_name = self.extract_resource_name(document_path);
56
57        let write = Write {
58            update_mask: None,
59            update_transforms: None,
60            current_document: None,
61            operation: WriteOperation::Update(Document {
62                name: resource_name,
63                fields,
64                create_time: String::new(), // Ignored on write
65                update_time: String::new(), // Ignored on write
66            }),
67        };
68
69        self.writes.lock().unwrap().push(write);
70        Ok(self)
71    }
72
73    /// Updates fields in the document referred to by `document_path`.
74    ///
75    /// If the document does not exist, the operation will fail.
76    ///
77    /// # Arguments
78    ///
79    /// * `document_path` - The path to the document to update.
80    /// * `value` - The data to update.
81    pub fn update<T: Serialize>(
82        &self,
83        document_path: &str,
84        value: &T,
85    ) -> Result<&Self, FirestoreError> {
86        let fields = convert_serializable_to_fields(value)?;
87        let resource_name = self.extract_resource_name(document_path);
88
89        let field_paths = fields.keys().cloned().collect();
90
91        let write = Write {
92            update_mask: Some(DocumentMask { field_paths }),
93            update_transforms: None,
94            current_document: Some(Precondition {
95                exists: Some(true),
96                update_time: None,
97            }),
98            operation: WriteOperation::Update(Document {
99                name: resource_name,
100                fields,
101                create_time: String::new(),
102                update_time: String::new(),
103            }),
104        };
105
106        self.writes.lock().unwrap().push(write);
107        Ok(self)
108    }
109
110    /// Deletes the document referred to by `document_path`.
111    ///
112    /// # Arguments
113    ///
114    /// * `document_path` - The path to the document to delete.
115    pub fn delete(&self, document_path: &str) -> Result<&Self, FirestoreError> {
116        let resource_name = self.extract_resource_name(document_path);
117
118        let write = Write {
119            update_mask: None,
120            update_transforms: None,
121            current_document: None,
122            operation: WriteOperation::Delete(resource_name),
123        };
124
125        self.writes.lock().unwrap().push(write);
126        Ok(self)
127    }
128
129    /// Creates a document at the given path.
130    ///
131    /// If the document already exists, the operation will fail.
132    ///
133    /// # Arguments
134    ///
135    /// * `document_path` - The path to the document to create.
136    /// * `value` - The data to write.
137    pub fn create<T: Serialize>(
138        &self,
139        document_path: &str,
140        value: &T,
141    ) -> Result<&Self, FirestoreError> {
142        let fields = convert_serializable_to_fields(value)?;
143        let resource_name = self.extract_resource_name(document_path);
144
145        let write = Write {
146            update_mask: None,
147            update_transforms: None,
148            current_document: Some(Precondition {
149                exists: Some(false),
150                update_time: None,
151            }),
152            operation: WriteOperation::Update(Document {
153                name: resource_name,
154                fields,
155                create_time: String::new(),
156                update_time: String::new(),
157            }),
158        };
159
160        self.writes.lock().unwrap().push(write);
161        Ok(self)
162    }
163
164    fn extract_resource_name(&self, document_path: &str) -> String {
165        let prefix = "https://firestore.googleapis.com/v1/";
166        let base_path = self.base_url.strip_prefix(prefix).unwrap_or(&self.base_url);
167        format!("{}/{}", base_path, document_path)
168    }
169
170    /// Commits the batch of writes.
171    pub async fn commit(&self) -> Result<Vec<WriteResult>, FirestoreError> {
172        let writes = {
173            let mut guard = self.writes.lock().unwrap();
174            let w = guard.clone();
175            guard.clear();
176            w
177        };
178
179        if writes.is_empty() {
180            return Ok(Vec::new());
181        }
182
183        let url = format!("{}:commit", self.base_url.split("/documents").next().unwrap());
184
185        let request = CommitRequest {
186            transaction: None,
187            writes,
188        };
189
190        let response = self
191            .client
192            .post(&url)
193            .header(header::CONTENT_TYPE, "application/json")
194            .body(serde_json::to_vec(&request)?)
195            .send()
196            .await?;
197
198        if !response.status().is_success() {
199            let status = response.status();
200            let text = response.text().await.unwrap_or_default();
201            return Err(FirestoreError::ApiError(format!(
202                "Commit batch failed {}: {}",
203                status, text
204            )));
205        }
206
207        let result: CommitResponse = response.json().await?;
208        Ok(result.write_results)
209    }
210}