firestore_db_and_auth/documents/
write.rs

1use super::*;
2
3/// This is returned by the write() method in a successful case.
4///
5/// This structure contains the document id of the written document.
6#[derive(Serialize, Deserialize)]
7pub struct WriteResult {
8    ///
9    pub create_time: Option<chrono::DateTime<chrono::Utc>>,
10    pub update_time: Option<chrono::DateTime<chrono::Utc>>,
11    pub document_id: String,
12}
13
14/// Write options. The default will overwrite a target document and not merge fields.
15#[derive(Default)]
16pub struct WriteOptions {
17    /// If this is set instead of overwriting all fields of a target document, only the given fields will be merged.
18    /// This only works if your document type has Option fields.
19    /// The write will fail, if no document_id is given or the target document does not exist yet.
20    pub merge: bool,
21}
22
23///
24/// Write a document to a given collection.
25///
26/// If no document_id is given, Firestore will generate an ID. Check the [`WriteResult`] return value.
27///
28/// If a document_id is given, the document will be created if it does not yet exist.
29/// Except if the "merge" option (see [`WriteOptions::merge`]) is set.
30///
31/// Example:
32///```no_run
33///use firestore_db_and_auth::{Credentials, ServiceSession, documents, errors::Result, FirebaseAuthBearer};
34///use serde::{Serialize,Deserialize};
35///# use firestore_db_and_auth::credentials::doctest_credentials;
36///
37/// #[derive(Serialize, Deserialize)]
38/// struct DemoDTO {
39///    a_string: String,
40///    an_int: u32,
41///    another_int: u32,
42/// }
43/// #[derive(Serialize, Deserialize)]
44/// struct DemoPartialDTO {
45///    #[serde(skip_serializing_if = "Option::is_none")]
46///    a_string: Option<String>,
47///    an_int: u32,
48/// }
49///
50/// async fn write(session: &impl FirebaseAuthBearer) -> Result<()> {
51///    let obj = DemoDTO { a_string: "abcd".to_owned(), an_int: 14, another_int: 10 };
52///    let result = documents::write(session, "tests", Some("service_test"), &obj, documents::WriteOptions::default()).await?;
53///    println!("id: {}, created: {}, updated: {}", result.document_id, result.create_time.unwrap(), result.update_time.unwrap());
54///    Ok(())
55/// }
56/// /// Only write some fields and do not overwrite the entire document.
57/// /// Either via Option<> or by not having the fields in the structure, see DemoPartialDTO.
58/// async fn write_partial(session: &impl FirebaseAuthBearer) -> Result<()> {
59///    let obj = DemoPartialDTO { a_string: None, an_int: 16 };
60///    let result = documents::write(session, "tests", Some("service_test"), &obj, documents::WriteOptions{merge:true}).await?;
61///    println!("id: {}, created: {}, updated: {}", result.document_id, result.create_time.unwrap(), result.update_time.unwrap());
62///    Ok(())
63/// }
64///
65/// # #[tokio::main]
66/// # async fn main() -> Result<()> {
67/// #   let session = ServiceSession::new(doctest_credentials().await).await?;
68/// #   write(&session).await?;
69/// #   write_partial(&session).await?;
70/// #
71/// #   Ok(())
72/// # }
73///```
74
75///
76/// ## Arguments
77/// * 'auth' The authentication token
78/// * 'path' The document path / collection; For example "my_collection" or "a/nested/collection"
79/// * 'document_id' The document id. Make sure that you do not include the document id in the path argument.
80/// * 'document' The document
81/// * 'options' Write options
82pub async fn write<T>(
83    auth: &impl FirebaseAuthBearer,
84    path: &str,
85    document_id: Option<impl AsRef<str>>,
86    document: &T,
87    options: WriteOptions,
88) -> Result<WriteResult>
89where
90    T: Serialize,
91{
92    let mut url = match document_id.as_ref() {
93        Some(document_id) => firebase_url_extended(auth.project_id(), path, document_id.as_ref()),
94        None => firebase_url(auth.project_id(), path),
95    };
96
97    let firebase_document = pod_to_document(&document)?;
98
99    if options.merge && firebase_document.fields.is_some() {
100        let fields = firebase_document.fields.as_ref().unwrap().keys().join(",");
101        url = format!("{}?currentDocument.exists=true&updateMask.fieldPaths={}", url, fields);
102    }
103
104    let builder = if document_id.is_some() {
105        auth.client().patch(&url)
106    } else {
107        auth.client().post(&url)
108    };
109
110    let resp = builder
111        .bearer_auth(auth.access_token().await.to_owned())
112        .json(&firebase_document)
113        .send()
114        .await?;
115
116    let resp = extract_google_api_error_async(resp, || {
117        document_id
118            .as_ref()
119            .and_then(|f| Some(f.as_ref().to_owned()))
120            .or(Some(String::new()))
121            .unwrap()
122    })
123    .await?;
124
125    let result_document: dto::Document = resp.json().await?;
126    let document_id = Path::new(&result_document.name)
127        .file_name()
128        .ok_or_else(|| FirebaseError::Generic("Resulting documents 'name' field is not a valid path"))?
129        .to_str()
130        .ok_or_else(|| FirebaseError::Generic("No valid unicode in 'name' field"))?
131        .to_owned();
132
133    let create_time = match result_document.create_time {
134        Some(f) => Some(
135            chrono::DateTime::parse_from_rfc3339(&f)
136                .map_err(|_| FirebaseError::Generic("Failed to parse rfc3339 date from 'create_time' field"))?
137                .with_timezone(&chrono::Utc),
138        ),
139        None => None,
140    };
141    let update_time = match result_document.update_time {
142        Some(f) => Some(
143            chrono::DateTime::parse_from_rfc3339(&f)
144                .map_err(|_| FirebaseError::Generic("Failed to parse rfc3339 date from 'update_time' field"))?
145                .with_timezone(&chrono::Utc),
146        ),
147        None => None,
148    };
149
150    Ok(WriteResult {
151        document_id,
152        create_time,
153        update_time,
154    })
155}