Skip to main content

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