diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 88ed6e1..680c5a8 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -467,6 +467,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "firestore-path" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9adf35365311153e4335025bc6efc46df5eca717296d0b150ac00b116b166bc4" +dependencies = [ + "thiserror", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1647,18 +1656,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" dependencies = [ "proc-macro2", "quote", @@ -2101,6 +2110,7 @@ dependencies = [ "async-graphql-axum", "axum", "features", + "firestore-path", "google-api-proto", "google-authz", "hyper", diff --git a/rust/crates/web/Cargo.toml b/rust/crates/web/Cargo.toml index ea5a973..49d04b1 100644 --- a/rust/crates/web/Cargo.toml +++ b/rust/crates/web/Cargo.toml @@ -11,6 +11,7 @@ async-graphql = "6.0.5" async-graphql-axum = "6.0.5" axum = "0.6.20" features = "0.10.0" +firestore-path = "0.4.0" google-api-proto = { version = "1.415.0", features = ["google-firestore-v1"] } google-authz = { version = "1.0.0-alpha.5", features = ["tonic"] } hyper = { version = "0.14.27", features = ["full"] } diff --git a/rust/crates/web/src/infra/firestore.rs b/rust/crates/web/src/infra/firestore.rs index b61b18a..a276ec2 100644 --- a/rust/crates/web/src/infra/firestore.rs +++ b/rust/crates/web/src/infra/firestore.rs @@ -1,10 +1,13 @@ pub mod client; pub mod document; -pub mod path; pub mod timestamp; #[cfg(test)] mod tests { + use std::str::FromStr as _; + + use firestore_path::{DatabaseId, DatabaseName, ProjectId}; + use crate::infra::firestore::{ client::{Client, Error}, document::Document, @@ -14,20 +17,22 @@ mod tests { async fn test() -> anyhow::Result<()> { let endpoint = "http://firebase:8080"; let mut client = Client::new( - "demo-project1".to_string(), - "(default)".to_string(), + DatabaseName::new( + ProjectId::from_str("demo-project1")?, + DatabaseId::from_str("(default)")?, + ), endpoint, ) .await?; - let collection_path = client.collection("repositories")?; + let collection_name = client.collection("repositories")?; assert_eq!( - collection_path.path(), + collection_name.to_string(), "projects/demo-project1/databases/(default)/documents/repositories" ); // reset - let (documents, _) = client.list::(&collection_path).await?; + let (documents, _) = client.list::(&collection_name).await?; for doc in documents { client.delete(doc.name(), doc.update_time()).await?; } @@ -40,10 +45,10 @@ mod tests { let input = V { k1: "v1".to_string(), }; - let document_path = collection_path.clone().doc("1")?; + let document_path = collection_name.clone().doc("1")?; let created = client.create(&document_path, input.clone()).await?; assert_eq!( - created.name().path(), + created.name().to_string(), "projects/demo-project1/databases/(default)/documents/repositories/1" ); assert_eq!(created.clone().data(), input); @@ -53,7 +58,7 @@ mod tests { assert_eq!(got, created); // READ (LIST) - let (documents, next_page_token) = client.list::(&collection_path).await?; + let (documents, next_page_token) = client.list::(&collection_name).await?; assert_eq!(documents, vec![got.clone()]); assert_eq!(next_page_token, None); @@ -84,20 +89,22 @@ mod tests { async fn test_transaction() -> anyhow::Result<()> { let endpoint = "http://firebase:8080"; let mut client = Client::new( - "demo-project1".to_string(), - "(default)".to_string(), + DatabaseName::new( + ProjectId::from_str("demo-project1")?, + DatabaseId::from_str("(default)")?, + ), endpoint, ) .await?; - let collection_path = client.collection("transactions")?; + let collection_name = client.collection("transactions")?; // reset - let (documents, _) = client.list::(&collection_path).await?; + let (documents, _) = client.list::(&collection_name).await?; for doc in documents { client.delete(doc.name(), doc.update_time()).await?; } - let document_path = collection_path.doc("1")?; + let document_name = collection_name.doc("1")?; #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] struct V { @@ -109,18 +116,18 @@ mod tests { }; client .run_transaction(|transaction| { - let p = document_path.clone(); + let p = document_name.clone(); Box::pin(async move { transaction.create(&p, input)?; Ok(()) }) }) .await?; - assert!(client.get::(&document_path).await.is_ok()); + assert!(client.get::(&document_name).await.is_ok()); let result = client .run_transaction(|transaction| { - let p = document_path.clone(); + let p = document_name.clone(); Box::pin(async move { let got = transaction.get::(&p).await?; transaction.delete(&p, got.update_time())?; @@ -130,20 +137,20 @@ mod tests { .await; assert!(result.is_err()); // Not deleted because it was rolled back - assert!(client.get::(&document_path).await.is_ok()); + assert!(client.get::(&document_name).await.is_ok()); - let got = client.get::(&document_path).await?; + let got = client.get::(&document_name).await?; let current_update_time = got.update_time(); client .run_transaction(|transaction| { - let p = document_path.clone(); + let p = document_name.clone(); Box::pin(async move { transaction.delete(&p, current_update_time)?; Ok(()) }) }) .await?; - let err = client.get::(&document_path).await.unwrap_err(); + let err = client.get::(&document_name).await.unwrap_err(); if let crate::infra::firestore::client::Error::Status(status) = err { assert_eq!(status.code(), tonic::Code::NotFound); } else { diff --git a/rust/crates/web/src/infra/firestore/client.rs b/rust/crates/web/src/infra/firestore/client.rs index 4bc3233..7e93715 100644 --- a/rust/crates/web/src/infra/firestore/client.rs +++ b/rust/crates/web/src/infra/firestore/client.rs @@ -1,5 +1,6 @@ use std::{future::Future, pin::Pin}; +use firestore_path::{CollectionName, CollectionPath, DatabaseName, DocumentName}; use google_api_proto::google::firestore::v1::{ firestore_client::FirestoreClient, get_document_request::ConsistencySelector, precondition::ConditionType, value::ValueType, write::Operation, BeginTransactionRequest, @@ -14,11 +15,7 @@ use tonic::transport::Channel; use crate::{infra::firestore::document, use_case}; -use super::{ - document::Document, - path::{self, CollectionPath, DocumentPath, RootPath}, - timestamp::Timestamp, -}; +use super::{document::Document, timestamp::Timestamp}; pub struct Transaction { client: Client, @@ -27,14 +24,14 @@ pub struct Transaction { } impl Transaction { - pub fn create(&mut self, document_path: &DocumentPath, fields: T) -> Result<(), Error> + pub fn create(&mut self, document_name: &DocumentName, fields: T) -> Result<(), Error> where T: Serialize, { self.writes.push(Write { operation: Some(Operation::Update( google_api_proto::google::firestore::v1::Document { - name: document_path.path(), + name: document_name.to_string(), fields: { let ser = to_value(&fields)?; if let Some(ValueType::MapValue(MapValue { fields })) = ser.value_type { @@ -58,11 +55,11 @@ impl Transaction { pub fn delete( &mut self, - document_path: &DocumentPath, + document_name: &DocumentName, current_update_time: Timestamp, ) -> Result<(), Error> { self.writes.push(Write { - operation: Some(Operation::Delete(document_path.path())), + operation: Some(Operation::Delete(document_name.to_string())), update_mask: None, update_transforms: vec![], current_document: Some(Precondition { @@ -74,7 +71,7 @@ impl Transaction { Ok(()) } - pub async fn get(&mut self, document_path: &DocumentPath) -> Result, Error> + pub async fn get(&mut self, document_name: &DocumentName) -> Result, Error> where U: DeserializeOwned, { @@ -82,7 +79,7 @@ impl Transaction { .client .client .get_document(GetDocumentRequest { - name: document_path.path(), + name: document_name.to_string(), mask: None, consistency_selector: Some(ConsistencySelector::Transaction( self.transaction.clone(), @@ -108,7 +105,7 @@ pub enum Error { #[error("deserialize {0}")] Deserialize(#[from] document::Error), #[error("path {0}")] - Path(#[from] path::Error), + Path(#[from] firestore_path::Error), #[error("serialize {0}")] Serialize(#[from] serde_firestore_value::Error), #[error("status {0}")] @@ -130,17 +127,13 @@ impl From for use_case::Error { #[derive(Clone, Debug)] pub struct Client { client: FirestoreClient>, - root_path: RootPath, + database_name: DatabaseName, } impl Client { // TODO: run_query - pub async fn new( - project_id: String, - database_id: String, - endpoint: &'static str, - ) -> Result { + pub async fn new(database_name: DatabaseName, endpoint: &'static str) -> Result { let credentials = Credentials::builder().no_credentials().build().await?; let channel = Channel::from_static(endpoint).connect().await?; let channel = GoogleAuthz::builder(channel) @@ -148,15 +141,17 @@ impl Client { .build() .await; let client = FirestoreClient::new(channel); - let root_path = RootPath::new(project_id, database_id)?; - Ok(Self { client, root_path }) + Ok(Self { + client, + database_name, + }) } pub async fn begin_transaction(&mut self) -> Result { let response = self .client .begin_transaction(BeginTransactionRequest { - database: self.root_path.database_name(), + database: self.database_name.to_string(), options: None, }) .await?; @@ -168,16 +163,16 @@ impl Client { }) } - pub fn collection(&self, collection_id: S) -> Result + pub fn collection(&self, collection_path: S) -> Result where - S: Into, + S: TryInto, { - Ok(self.root_path.clone().collection(collection_id)?) + Ok(self.database_name.clone().collection(collection_path)?) } pub async fn create( &mut self, - document_path: &DocumentPath, + document_name: &DocumentName, fields: T, ) -> Result, Error> where @@ -187,9 +182,14 @@ impl Client { let response = self .client .create_document(CreateDocumentRequest { - parent: document_path.parent().parent().path(), - collection_id: document_path.parent().id().to_string(), - document_id: document_path.id().to_string(), + parent: document_name + .clone() + .parent() + .parent() + .map(|parent| parent.to_string()) + .unwrap_or_else(|| document_name.database_name().to_string()), + collection_id: document_name.collection_id().to_string(), + document_id: document_name.document_id().to_string(), document: Some(google_api_proto::google::firestore::v1::Document { name: "".to_string(), fields: { @@ -211,12 +211,12 @@ impl Client { pub async fn delete( &mut self, - document_path: &DocumentPath, + document_name: &DocumentName, current_update_time: Timestamp, ) -> Result<(), Error> { self.client .delete_document(DeleteDocumentRequest { - name: document_path.path(), + name: document_name.to_string(), current_document: Some(Precondition { condition_type: Some(ConditionType::UpdateTime(prost_types::Timestamp::from( current_update_time, @@ -227,14 +227,14 @@ impl Client { Ok(()) } - pub async fn get(&mut self, document_path: &DocumentPath) -> Result, Error> + pub async fn get(&mut self, document_name: &DocumentName) -> Result, Error> where U: DeserializeOwned, { let response = self .client .get_document(GetDocumentRequest { - name: document_path.path(), + name: document_name.to_string(), mask: None, consistency_selector: None, }) @@ -245,7 +245,7 @@ impl Client { // TODO: support some params pub async fn list( &mut self, - collection_path: &CollectionPath, + collection_name: &CollectionName, ) -> Result<(Vec>, Option), Error> where U: DeserializeOwned, @@ -253,8 +253,12 @@ impl Client { let response = self .client .list_documents(ListDocumentsRequest { - parent: collection_path.parent().path(), - collection_id: collection_path.id().to_string(), + parent: collection_name + .clone() + .parent() + .map(|parent| parent.to_string()) + .unwrap_or_else(|| collection_name.database_name().to_string()), + collection_id: collection_name.collection_id().to_string(), page_size: 100, ..Default::default() }) @@ -291,7 +295,7 @@ impl Client { let response = self .client .begin_transaction(BeginTransactionRequest { - database: self.root_path.database_name(), + database: self.database_name.to_string(), options: None, }) .await?; @@ -306,7 +310,7 @@ impl Client { let response = self .client .commit(CommitRequest { - database: self.root_path.database_name(), + database: self.database_name.to_string(), writes: transaction.writes, transaction: transaction.transaction, }) @@ -319,7 +323,7 @@ impl Client { match self .client .rollback(RollbackRequest { - database: self.root_path.database_name(), + database: self.database_name.to_string(), transaction: transaction.transaction, }) .await @@ -336,7 +340,7 @@ impl Client { pub async fn update( &mut self, - document_path: &DocumentPath, + document_name: &DocumentName, fields: T, current_update_time: Timestamp, ) -> Result, Error> @@ -348,7 +352,7 @@ impl Client { .client .update_document(UpdateDocumentRequest { document: Some(google_api_proto::google::firestore::v1::Document { - name: document_path.path(), + name: document_name.to_string(), fields: { let ser = to_value(&fields)?; if let Some(ValueType::MapValue(MapValue { fields })) = ser.value_type { diff --git a/rust/crates/web/src/infra/firestore/document.rs b/rust/crates/web/src/infra/firestore/document.rs index c2c700a..191a275 100644 --- a/rust/crates/web/src/infra/firestore/document.rs +++ b/rust/crates/web/src/infra/firestore/document.rs @@ -1,10 +1,11 @@ use std::str::FromStr; +use firestore_path::DocumentName; use google_api_proto::google::firestore::v1::{ value::ValueType, Document as FirestoreDocument, MapValue, Value, }; -use super::{path::DocumentPath, timestamp::Timestamp}; +use super::timestamp::Timestamp; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -15,14 +16,14 @@ pub enum Error { #[error("deserialize")] Deserialize(#[from] serde_firestore_value::Error), #[error("invalid name")] - InvalidName(#[from] crate::infra::firestore::path::Error), + InvalidName(#[from] firestore_path::Error), } #[derive(Clone, Debug, Eq, PartialEq)] pub struct Document { create_time: Timestamp, data: T, - name: DocumentPath, + name: DocumentName, update_time: Timestamp, } @@ -39,7 +40,7 @@ impl Document { let data: T = serde_firestore_value::from_value(&Value { value_type: Some(ValueType::MapValue(MapValue { fields })), })?; - let name = DocumentPath::from_str(name.as_str())?; + let name = DocumentName::from_str(name.as_str())?; let update_time = Timestamp::from(update_time.ok_or(Error::UpdateTimeIsNone)?); Ok(Self { create_time, @@ -57,7 +58,7 @@ impl Document { self.data } - pub fn name(&self) -> &DocumentPath { + pub fn name(&self) -> &DocumentName { &self.name } diff --git a/rust/crates/web/src/infra/firestore/path.rs b/rust/crates/web/src/infra/firestore/path.rs deleted file mode 100644 index a3308d9..0000000 --- a/rust/crates/web/src/infra/firestore/path.rs +++ /dev/null @@ -1,508 +0,0 @@ -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("invalid root path")] - InvalidRootPath, - #[error("not collection path")] - InvalidCollectionPath, - #[error("not document path")] - InvalidDocumentPath, - #[error("too long")] - TooLong, -} - -#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub enum Path { - Collection(CollectionPath), - Document(DocumentPath), - Root(RootPath), -} - -impl Path { - pub fn path(&self) -> String { - match self { - Path::Collection(p) => p.path(), - Path::Document(p) => p.path(), - Path::Root(p) => p.path(), - } - } - - pub fn root(&self) -> &RootPath { - match self { - Path::Collection(p) => p.root(), - Path::Document(p) => p.root(), - Path::Root(p) => p, - } - } -} - -impl From for Path { - fn from(value: CollectionPath) -> Self { - Self::Collection(value) - } -} - -impl From for Path { - fn from(value: DocumentPath) -> Self { - Self::Document(value) - } -} - -impl From for Path { - fn from(value: RootPath) -> Self { - Self::Root(value) - } -} - -impl std::str::FromStr for Path { - type Err = Error; - - fn from_str(s: &str) -> Result { - from_str(s) - } -} - -#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub struct CollectionPath { - id: String, - parent: Box, -} - -impl CollectionPath { - pub fn doc(self, document_id: S) -> Result - where - S: Into, - { - // TODO: check document_id format - Ok(DocumentPath { - id: document_id.into(), - parent: self, - }) - } - - pub fn id(&self) -> &str { - self.id.as_str() - } - - pub fn parent(&self) -> &Path { - self.parent.as_ref() - } - - pub fn path(&self) -> String { - format!("{}/{}", self.parent.path(), self.id) - } - - pub fn root(&self) -> &RootPath { - self.parent.root() - } -} - -impl std::str::FromStr for CollectionPath { - type Err = Error; - - fn from_str(s: &str) -> Result { - if let Path::Collection(p) = from_str(s)? { - Ok(p) - } else { - Err(Error::InvalidCollectionPath) - } - } -} - -#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub struct DocumentPath { - id: String, - parent: CollectionPath, -} - -impl DocumentPath { - pub fn collection(self, collection_id: S) -> Result - where - S: Into, - { - // TODO: check collection_id format - Ok(CollectionPath { - id: collection_id.into(), - parent: Box::new(Path::from(self)), - }) - } - - pub fn id(&self) -> &str { - self.id.as_str() - } - - pub fn parent(&self) -> &CollectionPath { - &self.parent - } - - pub fn path(&self) -> String { - format!("{}/{}", self.parent.path(), self.id) - } - - pub fn root(&self) -> &RootPath { - self.parent.root() - } -} - -impl std::str::FromStr for DocumentPath { - type Err = Error; - - fn from_str(s: &str) -> Result { - if let Path::Document(p) = from_str(s)? { - Ok(p) - } else { - Err(Error::InvalidDocumentPath) - } - } -} - -#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub struct RootPath { - database_id: String, - project_id: String, -} - -impl RootPath { - pub fn new(project_id: P, database_id: D) -> Result - where - P: Into, - D: Into, - { - // TODO: check project_id and database_id format - Ok(Self { - database_id: database_id.into(), - project_id: project_id.into(), - }) - } - - pub fn collection(self, collection_id: S) -> Result - where - S: Into, - { - // TODO: check collection_id format - Ok(CollectionPath { - id: collection_id.into(), - parent: Box::new(Path::from(self)), - }) - } - - pub fn database_id(&self) -> &str { - self.database_id.as_str() - } - - pub fn database_name(&self) -> String { - format!( - "projects/{}/databases/{}", - self.project_id, self.database_id - ) - } - - pub fn path(&self) -> String { - format!( - "projects/{}/databases/{}/documents", - self.project_id, self.database_id - ) - } - - pub fn project_id(&self) -> &str { - self.project_id.as_str() - } -} - -impl std::str::FromStr for RootPath { - type Err = Error; - - fn from_str(s: &str) -> Result { - if let Path::Root(p) = from_str(s)? { - Ok(p) - } else { - Err(Error::InvalidRootPath) - } - } -} - -fn from_str(s: &str) -> Result { - if s.len() > 1_024 * 6 { - return Err(Error::TooLong); - } - - let parts = s.split('/').collect::>(); - if parts.len() < 5 - || parts[0] != "projects" - || parts[2] != "databases" - || parts[4] != "documents" - { - return Err(Error::InvalidRootPath); - } - - // TODO: check Maximum depth of subcollections (<= 100) - - // TODO: check `"."` and `".."` and `"__.*__"` - // TODO: check len (<= 1500) - let mut path = Path::from(RootPath { - database_id: parts[3].to_string(), - project_id: parts[1].to_string(), - }); - for s in parts.into_iter().skip(5).map(|s| s.to_string()) { - path = match path { - Path::Collection(p) => Path::from(p.doc(s)?), - Path::Document(p) => Path::from(p.collection(s)?), - Path::Root(p) => Path::from(p.collection(s)?), - }; - } - Ok(path) -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use super::*; - - #[test] - fn test_collection_path_doc() -> anyhow::Result<()> { - let colleciton_path = RootPath::new("demo-project1", "(default)")?.collection("users")?; - assert_eq!( - colleciton_path.clone().doc("1")?.path(), - "projects/demo-project1/databases/(default)/documents/users/1" - ); - assert_eq!( - colleciton_path.doc("1".to_string())?.path(), - "projects/demo-project1/databases/(default)/documents/users/1" - ); - Ok(()) - } - - #[test] - fn test_document_path_collection() -> anyhow::Result<()> { - let document_path = RootPath::new("demo-project1", "(default)")? - .collection("users")? - .doc("1")?; - assert_eq!( - document_path.clone().collection("repositories")?.path(), - "projects/demo-project1/databases/(default)/documents/users/1/repositories" - ); - assert_eq!( - document_path.collection("repositories".to_string())?.path(), - "projects/demo-project1/databases/(default)/documents/users/1/repositories" - ); - Ok(()) - } - - #[test] - fn test_root_path_collection() -> anyhow::Result<()> { - let root_path = RootPath::new("demo-project1", "(default)")?; - assert_eq!( - root_path.clone().collection("users")?.path(), - "projects/demo-project1/databases/(default)/documents/users" - ); - assert_eq!( - root_path.collection("users".to_string())?.path(), - "projects/demo-project1/databases/(default)/documents/users" - ); - Ok(()) - } - - #[test] - fn test_root_path_new() -> anyhow::Result<()> { - let root_path = RootPath::new("demo-project1", "(default)")?; - assert_eq!( - root_path.path(), - "projects/demo-project1/databases/(default)/documents" - ); - let root_path = RootPath::new("demo-project1".to_string(), "(default)".to_string())?; - assert_eq!( - root_path.path(), - "projects/demo-project1/databases/(default)/documents" - ); - Ok(()) - } - - #[test] - fn test_root_path_from_str() -> anyhow::Result<()> { - // 6KiB - let s = format!( - "{}/{}/{}/{}/{}/{}/{}", - "projects/demo-project1/databases/(default)/documents", - "1".repeat(1024), - "2".repeat(1024), - "3".repeat(1024), - "4".repeat(1024), - "5".repeat(1024), - "6".repeat(1024 - 58) - ); - assert_eq!(s.len(), 1_024 * 6); - assert!(Path::from_str(&s).is_ok()); - let s = format!("{}a", s); - assert_eq!(s.len(), 1_024 * 6 + 1); - assert!(Path::from_str(&s).is_err()); - - assert!(Path::from_str("projects1/demo-project1/databases/(default)/documents").is_err()); - assert!(Path::from_str("projects/demo-project1/databases1/(default)/documents").is_err()); - assert!(Path::from_str("projects/demo-project1/databases/(default)/documents1").is_err()); - - let path = Path::from_str("projects/demo-project1/databases/(default)/documents")?; - assert_eq!( - path, - Path::Root(RootPath { - database_id: "(default)".to_string(), - project_id: "demo-project1".to_string(), - }) - ); - - let path = Path::from_str("projects/demo-project1/databases/(default)/documents/users")?; - assert_eq!( - path, - Path::Collection(CollectionPath { - id: "users".to_string(), - parent: Box::new(Path::Root(RootPath { - database_id: "(default)".to_string(), - project_id: "demo-project1".to_string(), - })) - }) - ); - - let path = Path::from_str("projects/demo-project1/databases/(default)/documents/users/1")?; - assert_eq!( - path, - Path::Document(DocumentPath { - id: "1".to_string(), - parent: CollectionPath { - id: "users".to_string(), - parent: Box::new(Path::Root(RootPath { - database_id: "(default)".to_string(), - project_id: "demo-project1".to_string(), - })) - } - }) - ); - - let path = Path::from_str( - "projects/demo-project1/databases/(default)/documents/users/1/repositories", - )?; - assert_eq!( - path, - Path::Collection(CollectionPath { - id: "repositories".to_string(), - parent: Box::new(Path::Document(DocumentPath { - id: "1".to_string(), - parent: CollectionPath { - id: "users".to_string(), - parent: Box::new(Path::Root(RootPath { - database_id: "(default)".to_string(), - project_id: "demo-project1".to_string(), - })) - } - })) - }) - ); - - let path = Path::from_str( - "projects/demo-project1/databases/(default)/documents/users/1/repositories/2", - )?; - assert_eq!( - path, - Path::Document(DocumentPath { - id: "2".to_string(), - parent: CollectionPath { - id: "repositories".to_string(), - parent: Box::new(Path::Document(DocumentPath { - id: "1".to_string(), - parent: CollectionPath { - id: "users".to_string(), - parent: Box::new(Path::Root(RootPath { - database_id: "(default)".to_string(), - project_id: "demo-project1".to_string(), - })) - } - })) - } - }) - ); - - Ok(()) - } - - #[test] - fn test() -> anyhow::Result<()> { - // root_path - let root_path = RootPath { - database_id: "(default)".to_string(), - project_id: "demo-project1".to_string(), - }; - assert_eq!(root_path.database_id(), "(default)"); - assert_eq!( - root_path.database_name(), - "projects/demo-project1/databases/(default)" - ); - assert_eq!( - root_path.path(), - "projects/demo-project1/databases/(default)/documents" - ); - assert_eq!(root_path.project_id(), "demo-project1"); - - // collection_path - let collection_path = root_path.collection("users")?; - assert_eq!(collection_path.id(), "users"); - assert_eq!( - collection_path.parent().path(), - "projects/demo-project1/databases/(default)/documents" - ); - assert_eq!( - collection_path.path(), - "projects/demo-project1/databases/(default)/documents/users" - ); - assert_eq!( - collection_path.root().path(), - "projects/demo-project1/databases/(default)/documents" - ); - - // document_path - let document_path = collection_path.doc("1")?; - assert_eq!(document_path.id(), "1"); - assert_eq!( - document_path.parent().path(), - "projects/demo-project1/databases/(default)/documents/users" - ); - assert_eq!( - document_path.path(), - "projects/demo-project1/databases/(default)/documents/users/1" - ); - assert_eq!( - document_path.root().path(), - "projects/demo-project1/databases/(default)/documents" - ); - - // collection_path (nested) - let nested_collection_path = document_path.collection("repositories")?; - assert_eq!(nested_collection_path.id(), "repositories"); - assert_eq!( - nested_collection_path.parent().path(), - "projects/demo-project1/databases/(default)/documents/users/1" - ); - assert_eq!( - nested_collection_path.path(), - "projects/demo-project1/databases/(default)/documents/users/1/repositories" - ); - assert_eq!( - nested_collection_path.root().path(), - "projects/demo-project1/databases/(default)/documents" - ); - - // document_path (nested) - let nested_document_path = nested_collection_path.doc("2")?; - assert_eq!(nested_document_path.id(), "2"); - assert_eq!( - nested_document_path.parent().path(), - "projects/demo-project1/databases/(default)/documents/users/1/repositories" - ); - assert_eq!( - nested_document_path.path(), - "projects/demo-project1/databases/(default)/documents/users/1/repositories/2" - ); - assert_eq!( - nested_document_path.root().path(), - "projects/demo-project1/databases/(default)/documents" - ); - Ok(()) - } -} diff --git a/rust/crates/web/src/infra/firestore_store.rs b/rust/crates/web/src/infra/firestore_store.rs index ce383b6..0b9749c 100644 --- a/rust/crates/web/src/infra/firestore_store.rs +++ b/rust/crates/web/src/infra/firestore_store.rs @@ -53,19 +53,32 @@ impl From for model::Item { } } +#[derive(Debug, thiserror::Error)] +enum Error { + #[error("client {0}")] + Client(#[from] super::firestore::client::Error), + #[error("invalid path {0}")] + InvalidPath(#[from] firestore_path::Error), +} + +impl From for use_case::Error { + fn from(e: Error) -> Self { + Self::Unknown(e.to_string()) + } +} + #[derive(Clone, Debug)] pub struct FirestoreStore { client: Arc>, } -#[async_trait] -impl Store for FirestoreStore { - async fn find_all_check_lists(&self) -> Result, use_case::Error> { +impl FirestoreStore { + async fn find_all_check_lists(&self) -> Result, Error> { let mut client = self.client.lock().await; - let collection_path = client.collection("check_lists")?; + let collection_name = client.collection("check_lists")?; // TODO: pagination Ok(client - .list::(&collection_path) + .list::(&collection_name) .await? .0 .into_iter() @@ -74,12 +87,11 @@ impl Store for FirestoreStore { .collect()) } - // TODO: remove - async fn find_all_checks(&self) -> Result, use_case::Error> { + async fn find_all_checks(&self) -> Result, Error> { let mut client = self.client.lock().await; - let collection_path = client.collection("checks")?; + let collection_name = client.collection("checks")?; Ok(client - .list::(&collection_path) + .list::(&collection_name) .await? .0 .into_iter() @@ -88,12 +100,12 @@ impl Store for FirestoreStore { .collect()) } - async fn find_all_items(&self) -> Result, use_case::Error> { + async fn find_all_items(&self) -> Result, Error> { let mut client = self.client.lock().await; - let collection_path = client.collection("items")?; + let collection_name = client.collection("items")?; // TODO: pagination Ok(client - .list::(&collection_path) + .list::(&collection_name) .await? .0 .into_iter() @@ -129,8 +141,42 @@ impl Store for FirestoreStore { } } +#[async_trait] +impl Store for FirestoreStore { + async fn find_all_check_lists(&self) -> Result, use_case::Error> { + Ok(self.find_all_check_lists().await?) + } + + // TODO: remove + async fn find_all_checks(&self) -> Result, use_case::Error> { + Ok(self.find_all_checks().await?) + } + + async fn find_all_items(&self) -> Result, use_case::Error> { + Ok(self.find_all_items().await?) + } + + async fn find_checks_by_check_list_id( + &self, + check_list_id: String, + ) -> Result, use_case::Error> { + Ok(self.find_checks_by_check_list_id(check_list_id).await?) + } + + async fn find_checks_by_item_id( + &self, + item_id: String, + ) -> Result, use_case::Error> { + Ok(self.find_checks_by_item_id(item_id).await?) + } +} + #[cfg(test)] mod tests { + use std::str::FromStr as _; + + use firestore_path::{DatabaseId, DatabaseName, ProjectId}; + use crate::infra::firestore::document::Document; use super::*; @@ -139,13 +185,15 @@ mod tests { async fn test_find_all_check_lists() -> anyhow::Result<()> { let endpoint = "http://firebase:8080"; let mut client = Client::new( - "demo-project1".to_string(), - "(default)".to_string(), + DatabaseName::new( + ProjectId::from_str("demo-project1")?, + DatabaseId::from_str("(default)")?, + ), endpoint, ) .await?; let collection = client.collection("check_lists")?; - let doc = collection.doc("1")?; + let doc = collection.clone().doc("1")?; let input = CheckListDocumentData { date: "2020-01-02".to_string(), @@ -174,13 +222,15 @@ mod tests { async fn test_find_all_items() -> anyhow::Result<()> { let endpoint = "http://firebase:8080"; let mut client = Client::new( - "demo-project1".to_string(), - "(default)".to_string(), + DatabaseName::new( + ProjectId::from_str("demo-project1")?, + DatabaseId::from_str("(default)")?, + ), endpoint, ) .await?; let collection = client.collection("items")?; - let doc = collection.doc("1")?; + let doc = collection.clone().doc("1")?; let input = ItemDocumentData { id: "1".to_string(),