Skip to main content

firebase_admin_sdk/firestore/
transaction.rs

1use super::models::{
2    CommitRequest, CommitResponse, Document, DocumentMask, Precondition, Write, WriteOperation,
3    WriteResult,
4};
5use super::reference::{convert_fields_to_serde_value, convert_serializable_to_fields};
6use super::FirestoreError;
7use crate::core::parse_error_response;
8use reqwest::header;
9use reqwest_middleware::ClientWithMiddleware;
10use serde::de::DeserializeOwned;
11use serde::Serialize;
12use std::sync::{Arc, Mutex};
13use url::Url;
14
15/// Represents a Firestore Transaction.
16///
17/// Transactions provide a way to ensure that a set of reads and writes are executed atomically.
18#[derive(Clone)]
19pub struct Transaction {
20    client: ClientWithMiddleware,
21    base_url: String,
22    pub transaction_id: String,
23    writes: Arc<Mutex<Vec<Write>>>,
24}
25
26impl Transaction {
27    pub(crate) fn new(
28        client: ClientWithMiddleware,
29        base_url: String,
30        transaction_id: String,
31    ) -> Self {
32        Self {
33            client,
34            base_url,
35            transaction_id,
36            writes: Arc::new(Mutex::new(Vec::new())),
37        }
38    }
39
40    /// Reads the document at the given path.
41    ///
42    /// The read is performed as part of the transaction.
43    ///
44    /// # Arguments
45    ///
46    /// * `document_path` - The path to the document to read.
47    pub async fn get<T: DeserializeOwned>(
48        &self,
49        document_path: &str,
50    ) -> Result<Option<T>, FirestoreError> {
51        // Construct the URL. Note that Firestore document paths in the API need to include the full resource name.
52        // However, the `document_path` passed here is usually relative (e.g. "users/alice").
53        // But the `base_url` is `https://firestore.../documents`.
54        // So we append the relative path.
55        let url = format!("{}/{}", self.base_url, document_path);
56        let mut url_obj = Url::parse(&url).map_err(|e| FirestoreError::ApiError(e.to_string()))?;
57        url_obj.query_pairs_mut().append_pair("transaction", &self.transaction_id);
58
59        // Add the transaction ID query parameter
60        let response = self
61            .client
62            .get(url_obj)
63            .send()
64            .await?;
65
66        if response.status() == reqwest::StatusCode::NOT_FOUND {
67            return Ok(None);
68        }
69
70        if !response.status().is_success() {
71            return Err(FirestoreError::ApiError(parse_error_response(response, "Get document in transaction failed").await));
72        }
73
74        let doc: Document = response.json().await?;
75        let serde_value = convert_fields_to_serde_value(doc.fields)?;
76        let obj = serde_json::from_value(serde_value)?;
77        Ok(Some(obj))
78    }
79
80    /// Overwrites the document referred to by `document_path`.
81    ///
82    /// If the document does not exist, it will be created. If it does exist, it will be overwritten.
83    ///
84    /// # Arguments
85    ///
86    /// * `document_path` - The path to the document to write.
87    /// * `value` - The data to write.
88    pub fn set<T: Serialize>(
89        &self,
90        document_path: &str,
91        value: &T,
92    ) -> Result<&Self, FirestoreError> {
93        let fields = convert_serializable_to_fields(value)?;
94        let resource_name = self.extract_resource_name(document_path);
95
96        let write = Write {
97            update_mask: None,
98            update_transforms: None,
99            current_document: None,
100            operation: WriteOperation::Update(Document {
101                name: resource_name,
102                fields,
103                create_time: String::new(), // Ignored on write
104                update_time: String::new(), // Ignored on write
105            }),
106        };
107
108        self.writes.lock().unwrap().push(write);
109        Ok(self)
110    }
111
112    /// Updates fields in the document referred to by `document_path`.
113    ///
114    /// If the document does not exist, the transaction will fail.
115    ///
116    /// # Arguments
117    ///
118    /// * `document_path` - The path to the document to update.
119    /// * `value` - The data to update.
120    pub fn update<T: Serialize>(
121        &self,
122        document_path: &str,
123        value: &T,
124    ) -> Result<&Self, FirestoreError> {
125        let fields = convert_serializable_to_fields(value)?;
126        let resource_name = self.extract_resource_name(document_path);
127
128        // For update, we need to specify which fields we are updating to avoid overwriting everything else if we only pass a subset.
129        // However, if the user passes a struct, we usually assume they want to update all fields present in the struct.
130        // The `update` method in standard Firestore SDKs usually takes a map or key-value pairs and only updates those.
131        // If the user passes a struct here, `convert_serializable_to_fields` will return all fields in that struct.
132        // We should construct a FieldMask based on the keys in `fields`.
133
134        let field_paths = fields.keys().cloned().collect();
135
136        let write = Write {
137            update_mask: Some(DocumentMask { field_paths }),
138            update_transforms: None,
139            current_document: Some(Precondition {
140                exists: Some(true),
141                update_time: None,
142            }),
143            operation: WriteOperation::Update(Document {
144                name: resource_name,
145                fields,
146                create_time: String::new(),
147                update_time: String::new(),
148            }),
149        };
150
151        self.writes.lock().unwrap().push(write);
152        Ok(self)
153    }
154
155    /// Deletes the document referred to by `document_path`.
156    ///
157    /// # Arguments
158    ///
159    /// * `document_path` - The path to the document to delete.
160    pub fn delete(&self, document_path: &str) -> Result<&Self, FirestoreError> {
161        let resource_name = self.extract_resource_name(document_path);
162
163        let write = Write {
164            update_mask: None,
165            update_transforms: None,
166            current_document: None,
167            operation: WriteOperation::Delete(resource_name),
168        };
169
170        self.writes.lock().unwrap().push(write);
171        Ok(self)
172    }
173
174    fn extract_resource_name(&self, document_path: &str) -> String {
175        // base_url: https://firestore.googleapis.com/v1/projects/my-project/databases/(default)/documents
176        // document_path: users/alice
177        // result: projects/my-project/databases/(default)/documents/users/alice
178
179        let prefix = "https://firestore.googleapis.com/v1/";
180        let base_path = self.base_url.strip_prefix(prefix).unwrap_or(&self.base_url);
181        format!("{}/{}", base_path, document_path)
182    }
183
184    /// Commits the transaction.
185    ///
186    /// This is called automatically by `run_transaction`.
187    pub(crate) async fn commit(&self) -> Result<Vec<WriteResult>, FirestoreError> {
188        let writes = {
189            let mut guard = self.writes.lock().unwrap();
190            let w = guard.clone();
191            guard.clear();
192            w
193        };
194
195        if writes.is_empty() {
196            return Ok(Vec::new());
197        }
198
199        let url = format!("{}:commit", self.base_url.split("/documents").next().unwrap());
200
201        let request = CommitRequest {
202            transaction: Some(self.transaction_id.clone()),
203            writes,
204        };
205
206        let response = self
207            .client
208            .post(&url)
209            .header(header::CONTENT_TYPE, "application/json")
210            .body(serde_json::to_vec(&request)?)
211            .send()
212            .await?;
213
214        if !response.status().is_success() {
215            return Err(FirestoreError::ApiError(parse_error_response(response, "Commit transaction failed").await));
216        }
217
218        let result: CommitResponse = response.json().await?;
219        Ok(result.write_results)
220    }
221}