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}