use crate::error::{FirebaseError, FirestoreError};
use crate::firestore::document_reference::DocumentReference;
use crate::firestore::document_snapshot::DocumentSnapshot;
use crate::firestore::field_value::{proto, MapValue};
use crate::firestore::firestore::FirestoreInterceptor;
use proto::google::firestore::v1::firestore_client::FirestoreClient as GrpcClient;
use std::collections::HashMap;
use std::sync::Arc;
pub struct Transaction {
pub(crate) transaction_id: Vec<u8>,
pub(crate) firestore: Arc<crate::firestore::firestore::FirestoreInner>,
pub(crate) reads: HashMap<String, Option<DocumentSnapshot>>,
pub(crate) writes: Vec<TransactionWrite>,
pub(crate) has_writes: bool,
}
#[derive(Debug, Clone)]
#[allow(dead_code)] pub(crate) enum TransactionWrite {
Set { path: String, data: MapValue },
Update { path: String, data: MapValue },
Delete { path: String },
}
impl Transaction {
pub(crate) fn new(
transaction_id: Vec<u8>,
firestore: Arc<crate::firestore::firestore::FirestoreInner>,
) -> Self {
Self {
transaction_id,
firestore,
reads: HashMap::new(),
writes: Vec::new(),
has_writes: false,
}
}
pub async fn get(
&mut self,
document: &DocumentReference,
) -> Result<Option<DocumentSnapshot>, FirebaseError> {
if self.has_writes {
return Err(FirestoreError::InvalidArgument(
"Firestore transactions require all reads to be executed before all writes"
.to_string(),
)
.into());
}
let path = &document.path;
if let Some(cached) = self.reads.get(path) {
return Ok(cached.clone());
}
use crate::firestore::field_value::proto::google::firestore::v1::{
batch_get_documents_request::ConsistencySelector, BatchGetDocumentsRequest,
};
let database_path = format!(
"projects/{}/databases/{}",
self.firestore.project_id, self.firestore.database_id
);
let full_path = format!("{}/documents/{}", database_path, path);
let request = BatchGetDocumentsRequest {
database: database_path.clone(),
documents: vec![full_path],
consistency_selector: Some(ConsistencySelector::Transaction(
self.transaction_id.clone(),
)),
..Default::default()
};
let interceptor = FirestoreInterceptor {
auth_data: self.firestore.auth_data.clone(),
};
let mut client = GrpcClient::with_interceptor(self.firestore.channel.clone(), interceptor);
let mut stream = client
.batch_get_documents(request)
.await
.map_err(|e| FirestoreError::Connection(format!("Failed to get document: {}", e)))?
.into_inner();
use futures::StreamExt;
let response = stream
.next()
.await
.ok_or_else(|| FirestoreError::Internal("Document not found".to_string()))?
.map_err(|e| FirestoreError::Connection(format!("Failed to read response: {}", e)))?;
use crate::firestore::field_value::proto::google::firestore::v1::batch_get_documents_response::Result as BatchResult;
let snapshot = match response.result {
Some(BatchResult::Found(doc)) => {
let data = MapValue { fields: doc.fields };
Some(DocumentSnapshot {
reference: document.clone(),
data: Some(data),
metadata: crate::firestore::document_snapshot::SnapshotMetadata::default(),
})
}
Some(BatchResult::Missing(_)) => {
Some(DocumentSnapshot {
reference: document.clone(),
data: None,
metadata: crate::firestore::document_snapshot::SnapshotMetadata::default(),
})
}
None => {
return Err(FirestoreError::Unknown(-1).into());
}
};
self.reads.insert(path.clone(), snapshot.clone());
Ok(snapshot)
}
pub fn set(
&mut self,
document: &DocumentReference,
data: MapValue,
) -> Result<(), FirebaseError> {
self.has_writes = true;
self.writes.push(TransactionWrite::Set {
path: document.path.clone(),
data,
});
Ok(())
}
pub fn update(
&mut self,
document: &DocumentReference,
data: MapValue,
) -> Result<(), FirebaseError> {
self.has_writes = true;
self.writes.push(TransactionWrite::Update {
path: document.path.clone(),
data,
});
Ok(())
}
pub fn delete(&mut self, document: &DocumentReference) -> Result<(), FirebaseError> {
self.has_writes = true;
self.writes.push(TransactionWrite::Delete {
path: document.path.clone(),
});
Ok(())
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_transaction_tracks_reads() {
}
}