├── .github └── workflows │ ├── security-audit.yml │ └── tests.yml ├── .gitignore ├── CONTRIBUTING.md ├── COPYRIGHT.md ├── Cargo.toml ├── LICENSE ├── README.md ├── SECURITY.md ├── examples ├── aggregated-query.rs ├── batch-get-stream.rs ├── batch-write-simple.rs ├── batch-write-streaming.rs ├── caching_memory_collections.rs ├── caching_persistent_collections.rs ├── camel-case.rs ├── consistency-selector.rs ├── crud.rs ├── database_id_option.rs ├── document-transform.rs ├── dynamic_doc_level_crud.rs ├── explain-query.rs ├── generated-document-id.rs ├── group-query.rs ├── latlng.rs ├── list-collections.rs ├── list-docs.rs ├── listen-changes.rs ├── nearest-vector-query.rs ├── nested_collections.rs ├── partition-query.rs ├── query-with-cursor.rs ├── query.rs ├── read-write-transactions.rs ├── reference.rs ├── timestamp.rs ├── token_auth.rs ├── transactions.rs └── update-precondition.rs ├── renovate.json ├── src ├── cache │ ├── backends │ │ ├── memory_backend.rs │ │ ├── mod.rs │ │ └── persistent_backend.rs │ ├── cache_filter_engine.rs │ ├── cache_query_engine.rs │ ├── configuration.rs │ ├── mod.rs │ └── options.rs ├── db │ ├── aggregated_query.rs │ ├── batch_simple_writer.rs │ ├── batch_streaming_writer.rs │ ├── batch_writer.rs │ ├── consistency_selector.rs │ ├── create.rs │ ├── delete.rs │ ├── get.rs │ ├── list.rs │ ├── listen_changes.rs │ ├── listen_changes_state_storage.rs │ ├── mod.rs │ ├── options.rs │ ├── parent_path_builder.rs │ ├── precondition_models.rs │ ├── query.rs │ ├── query_models.rs │ ├── session_params.rs │ ├── transaction.rs │ ├── transaction_models.rs │ ├── transaction_ops.rs │ ├── transform_models.rs │ └── update.rs ├── errors.rs ├── firestore_document_functions.rs ├── firestore_meta.rs ├── firestore_serde │ ├── deserializer.rs │ ├── latlng_serializers.rs │ ├── mod.rs │ ├── null_serializers.rs │ ├── reference_serializers.rs │ ├── serializer.rs │ ├── timestamp_serializers.rs │ └── vector_serializers.rs ├── firestore_value.rs ├── fluent_api │ ├── delete_builder.rs │ ├── document_transform_builder.rs │ ├── insert_builder.rs │ ├── listing_builder.rs │ ├── mod.rs │ ├── select_aggregation_builder.rs │ ├── select_builder.rs │ ├── select_filter_builder.rs │ ├── tests │ │ └── mockdb.rs │ └── update_builder.rs ├── lib.rs ├── struct_path_macro.rs └── timestamp_utils.rs └── tests ├── caching_memory_test.rs ├── caching_persistent_test.rs ├── common └── mod.rs ├── complex-structure-serialize.rs ├── create-option-tests.rs ├── crud-integration-tests.rs ├── macro_path_test.rs ├── nested-collections-tests.rs ├── query-integration-tests.rs ├── transaction-tests.rs ├── transform_tests.rs └── update-precondition-test.rs /.github/workflows/security-audit.yml: -------------------------------------------------------------------------------- 1 | name: security audit 2 | on: 3 | push: 4 | paths: 5 | - '**/Cargo.toml' 6 | - '**/Cargo.lock' 7 | schedule: 8 | - cron: '5 4 * * 6' 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref_protected && github.run_id || github.event.pull_request.number || github.ref }} 11 | cancel-in-progress: true 12 | jobs: 13 | security_audit: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: dtolnay/rust-toolchain@stable 18 | with: 19 | toolchain: stable 20 | components: rustfmt, clippy 21 | - run: cargo install cargo-audit && cargo audit || true && cargo audit 22 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests & formatting 2 | on: 3 | push: 4 | pull_request: 5 | types: [ opened ] 6 | workflow_dispatch: 7 | env: 8 | GCP_PROJECT: latestbit 9 | GCP_PROJECT_ID: 288860578009 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref_protected && github.run_id || github.event.pull_request.number || github.ref }} 12 | cancel-in-progress: true 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: 'read' 18 | id-token: 'write' 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: dtolnay/rust-toolchain@stable 22 | with: 23 | toolchain: stable 24 | components: rustfmt, clippy 25 | - name: Authenticate to Google Cloud development 26 | id: auth 27 | uses: google-github-actions/auth@v2 28 | if: github.ref == 'refs/heads/master' 29 | with: 30 | workload_identity_provider: 'projects/${{ env.GCP_PROJECT_ID }}/locations/global/workloadIdentityPools/lb-github-identity-pool/providers/lb-github-identity-pool-provider' 31 | service_account: 'lb-github-service-account@${{ env.GCP_PROJECT }}.iam.gserviceaccount.com' 32 | create_credentials_file: true 33 | access_token_lifetime: '240s' 34 | - name: 'Set up Cloud SDK' 35 | uses: google-github-actions/setup-gcloud@v2 36 | if: github.ref == 'refs/heads/master' 37 | - name: 'Checking formatting and clippy' 38 | run: cargo fmt -- --check && cargo clippy -- -Dwarnings 39 | - name: 'Run lib tests' 40 | run: cargo test --lib --features "caching-memory,caching-persistent" 41 | if: github.ref != 'refs/heads/master' 42 | - name: 'Run all tests' 43 | run: cargo test --features "caching-memory,caching-persistent" 44 | if: github.ref == 'refs/heads/master' 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | Cargo.lock 3 | **/*.rs.bk 4 | .idea/ 5 | *.tmp 6 | *.orig 7 | *.swp 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Welcome! Please read this document to understand what you can do: 4 | * [Analyze Issues](#analyze-issues) 5 | * [Report an Issue](#report-an-issue) 6 | * [Contribute Code](#contribute-code) 7 | 8 | ## Analyze Issues 9 | 10 | Analyzing issue reports can be a lot of effort. Any help is welcome! 11 | Go to the GitHub issue tracker and find an open issue which needs additional work or a bugfix (e.g. issues labeled with "help wanted" or "bug"). 12 | Additional work could include any further information, or a gist, or it might be a hint that helps understanding the issue. 13 | 14 | ## Report an Issue 15 | 16 | If you find a bug - you are welcome to report it. 17 | You can go to the GitHub issue tracker to report the issue. 18 | 19 | ### Quick Checklist for Bug Reports 20 | 21 | Issue report checklist: 22 | * Real, current bug for the latest/supported version 23 | * No duplicate 24 | * Reproducible 25 | * Minimal example 26 | 27 | ### Issue handling process 28 | 29 | When an issue is reported, a committer will look at it and either confirm it as a real issue, close it if it is not an issue, or ask for more details. 30 | An issue that is about a real bug is closed as soon as the fix is committed. 31 | 32 | 33 | ### Reporting Security Issues 34 | 35 | If you find or suspect a security issue, please act responsibly and do not report it in the public issue tracker, but directly to us, so we can fix it before it can be exploited. 36 | For details please check our [Security policy](SECURITY.md). 37 | 38 | ## Contribute Code 39 | 40 | You are welcome to contribute code in order to fix bugs or to implement new features. 41 | 42 | There are three important things to know: 43 | 44 | 1. You must be aware of the Apache License (which describes contributions) and **agree to the Contributors License Agreement**. This is common practice in all major Open Source projects. 45 | For company contributors special rules apply. See the respective section below for details. 46 | 2. **Not all proposed contributions can be accepted**. Some features may e.g. just fit a third-party add-on better. The code must fit the overall direction and really improve it. The more effort you invest, the better you should clarify in advance whether the contribution fits: the best way would be to just open an issue to discuss the feature you plan to implement (make it clear you intend to contribute). 47 | 48 | ### Contributor License Agreement 49 | 50 | When you contribute (code, documentation, or anything else), you have to be aware that your contribution is covered by the same [Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0). 51 | 52 | This applies to all contributors, including those contributing on behalf of a company. 53 | 54 | ### Contribution Content Guidelines 55 | 56 | These are some of the rules we try to follow: 57 | 58 | - Apply a clean coding style adapted to the surrounding code, even though we are aware the existing code is not fully clean 59 | - Use variable naming conventions like in the other files you are seeing 60 | - No println() - use logging service if needed 61 | - Comment your code where it gets non-trivial 62 | - Keep an eye on performance and memory consumption, properly destroy objects when not used anymore 63 | - Avoid incompatible changes if possible, especially do not modify the name or behavior of public API methods or properties 64 | 65 | ### How to contribute - the Process 66 | 67 | 1. Make sure the change would be welcome (e.g. a bugfix or a useful feature); best do so by proposing it in a GitHub issue 68 | 2. Create a branch forking the repository and do your change 69 | 3. Commit and push your changes on that branch 70 | 4. In the commit message 71 | - Describe the problem you fix with this change. 72 | - Describe the effect that this change has from a user's point of view. App crashes and lockups are pretty convincing for example, but not all bugs are that obvious and should be mentioned in the text. 73 | - Describe the technical details of what you changed. It is important to describe the change in a most understandable way so the reviewer is able to verify that the code is behaving as you intend it to. 74 | 5. Create a Pull Request 75 | 6. Once the change has been approved we will inform you in a comment 76 | 7. We will close the pull request, feel free to delete the now obsolete branch 77 | -------------------------------------------------------------------------------- /COPYRIGHT.md: -------------------------------------------------------------------------------- 1 | Copyright 2022 Abdulla Abdurakhmanov (me@abdolence.dev) 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "firestore" 3 | version = "0.45.1" 4 | authors = ["Abdulla Abdurakhmanov "] 5 | edition = "2021" 6 | rust-version = "1.64" 7 | license = "Apache-2.0" 8 | description = "Library provides a simple API for Google Firestore and own Serde serializer based on efficient gRPC API" 9 | homepage = "https://github.com/abdolence/firestore-rs" 10 | repository = "https://github.com/abdolence/firestore-rs" 11 | documentation = "https://docs.rs/firestore" 12 | keywords = ["firestore", "google", "client"] 13 | categories = ["api-bindings"] 14 | readme = "README.md" 15 | include = ["Cargo.toml", "src/**/*.rs", "README.md", "LICENSE"] 16 | 17 | [badges] 18 | maintenance = { status = "actively-developed" } 19 | 20 | [lib] 21 | name = "firestore" 22 | path = "src/lib.rs" 23 | 24 | [features] 25 | default = ["tls-roots"] 26 | caching = [] 27 | caching-memory = ["caching", "dep:moka"] 28 | caching-persistent = ["caching", "dep:redb"] 29 | tls-roots = ["gcloud-sdk/tls-roots"] 30 | tls-webpki-roots = ["gcloud-sdk/tls-webpki-roots"] 31 | 32 | [dependencies] 33 | tracing = "0.1" 34 | gcloud-sdk = { version = "0.27.0", default-features = false, features = ["google-firestore-v1"] } 35 | hyper = { version = "1" } 36 | struct-path = "0.2" 37 | rvstruct = "0.3.2" 38 | rsb_derive = "0.5" 39 | serde = { version = "1", features = ["derive"] } 40 | tokio = { version = "1" } 41 | tokio-stream = "0.1" 42 | futures = "0.3" 43 | chrono = { version = "0.4", features = ["serde", "clock"], default-features = false } 44 | async-trait = "0.1" 45 | hex = "0.4" 46 | backoff = { version = "0.4", features = ["tokio"] } 47 | redb = { version = "2.1", optional = true } 48 | moka = { version = "0.12", features = ["future"], optional = true } # Caching library 49 | rand = "0.9" 50 | 51 | [dev-dependencies] 52 | cargo-husky = { version = "1.5", default-features = false, features = ["run-for-all", "prepush-hook", "run-cargo-fmt"] } 53 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 54 | tokio = { version = "1", features = ["full"] } 55 | tempfile = "3" 56 | approx = "0.5" 57 | rustls = "0.23" 58 | 59 | [[example]] 60 | name = "caching_memory_collections" 61 | path = "examples/caching_memory_collections.rs" 62 | required-features = ["caching-memory"] 63 | 64 | [[example]] 65 | name = "caching_persistent_collections" 66 | path = "examples/caching_persistent_collections.rs" 67 | required-features = ["caching-persistent"] 68 | 69 | [[test]] 70 | name = "caching_memory_test" 71 | path = "tests/caching_memory_test.rs" 72 | required-features = ["caching-memory"] 73 | 74 | [[test]] 75 | name = "caching_persistent_test" 76 | path = "tests/caching_persistent_test.rs" 77 | required-features = ["caching-persistent"] 78 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please follow general guidlines defined here: 6 | https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html 7 | 8 | ## Contacts 9 | E-mail: me@abdolence.dev 10 | -------------------------------------------------------------------------------- /examples/aggregated-query.rs: -------------------------------------------------------------------------------- 1 | use firestore::*; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | pub fn config_env_var(name: &str) -> Result { 5 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 6 | } 7 | 8 | // Example structure to play with 9 | #[derive(Debug, Clone, Deserialize, Serialize)] 10 | struct MyAggTestStructure { 11 | counter: usize, 12 | calc_sum: Option, 13 | calc_avg: Option, 14 | } 15 | 16 | #[tokio::main] 17 | async fn main() -> Result<(), Box> { 18 | // Logging with debug enabled 19 | let subscriber = tracing_subscriber::fmt() 20 | .with_env_filter("firestore=debug") 21 | .finish(); 22 | tracing::subscriber::set_global_default(subscriber)?; 23 | 24 | // Create an instance 25 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 26 | 27 | const TEST_COLLECTION_NAME: &str = "test"; 28 | 29 | println!("Aggregated query a test collection as a stream"); 30 | 31 | let objs: Vec = db 32 | .fluent() 33 | .select() 34 | .from(TEST_COLLECTION_NAME) 35 | .aggregate(|a| { 36 | a.fields([ 37 | a.field(path!(MyAggTestStructure::counter)).count(), 38 | a.field(path!(MyAggTestStructure::calc_sum)).sum("some_num"), 39 | a.field(path!(MyAggTestStructure::calc_avg)).avg("some_num"), 40 | ]) 41 | }) 42 | .obj() 43 | .query() 44 | .await?; 45 | 46 | println!("{:?}", objs); 47 | 48 | Ok(()) 49 | } 50 | -------------------------------------------------------------------------------- /examples/batch-get-stream.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use firestore::*; 3 | use futures::stream::BoxStream; 4 | use serde::{Deserialize, Serialize}; 5 | use tokio_stream::StreamExt; 6 | 7 | pub fn config_env_var(name: &str) -> Result { 8 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 9 | } 10 | 11 | // Example structure to play with 12 | #[derive(Debug, Clone, Deserialize, Serialize)] 13 | struct MyTestStructure { 14 | some_id: String, 15 | some_string: String, 16 | one_more_string: String, 17 | some_num: u64, 18 | created_at: DateTime, 19 | } 20 | 21 | #[tokio::main] 22 | async fn main() -> Result<(), Box> { 23 | // Logging with debug enabled 24 | let subscriber = tracing_subscriber::fmt() 25 | .with_env_filter("firestore=debug") 26 | .finish(); 27 | tracing::subscriber::set_global_default(subscriber)?; 28 | 29 | // Create an instance 30 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 31 | 32 | const TEST_COLLECTION_NAME: &str = "test"; 33 | 34 | println!("Populating a test collection"); 35 | for i in 0..10 { 36 | let my_struct = MyTestStructure { 37 | some_id: format!("test-{}", i), 38 | some_string: "Test".to_string(), 39 | one_more_string: "Test2".to_string(), 40 | some_num: 42, 41 | created_at: Utc::now(), 42 | }; 43 | 44 | // Remove if it already exist 45 | db.fluent() 46 | .delete() 47 | .from(TEST_COLLECTION_NAME) 48 | .document_id(&my_struct.some_id) 49 | .execute() 50 | .await?; 51 | 52 | // Let's insert some data 53 | db.fluent() 54 | .insert() 55 | .into(TEST_COLLECTION_NAME) 56 | .document_id(&my_struct.some_id) 57 | .object(&my_struct) 58 | .execute::<()>() 59 | .await?; 60 | } 61 | 62 | println!("Getting objects by IDs as a stream"); 63 | // Query as a stream our data 64 | let mut object_stream: BoxStream<(String, Option)> = db 65 | .fluent() 66 | .select() 67 | .by_id_in(TEST_COLLECTION_NAME) 68 | .obj() 69 | .batch(vec!["test-0", "test-5"]) 70 | .await?; 71 | 72 | while let Some(object) = object_stream.next().await { 73 | println!("Object in stream: {:?}", object); 74 | } 75 | 76 | // Getting as a stream with errors when needed 77 | let mut object_stream_with_errors: BoxStream< 78 | FirestoreResult<(String, Option)>, 79 | > = db 80 | .fluent() 81 | .select() 82 | .by_id_in(TEST_COLLECTION_NAME) 83 | .obj() 84 | .batch_with_errors(vec!["test-0", "test-5"]) 85 | .await?; 86 | 87 | while let Some(object) = object_stream_with_errors.try_next().await? { 88 | println!("Object in stream: {:?}", object); 89 | } 90 | 91 | Ok(()) 92 | } 93 | -------------------------------------------------------------------------------- /examples/batch-write-simple.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use firestore::*; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | pub fn config_env_var(name: &str) -> Result { 6 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 7 | } 8 | 9 | #[derive(Debug, Clone, Deserialize, Serialize)] 10 | struct MyTestStructure { 11 | some_id: String, 12 | some_string: String, 13 | created_at: DateTime, 14 | } 15 | 16 | #[tokio::main] 17 | async fn main() -> Result<(), Box> { 18 | // Logging with debug enabled 19 | let subscriber = tracing_subscriber::fmt() 20 | .with_env_filter("firestore=debug") 21 | .finish(); 22 | tracing::subscriber::set_global_default(subscriber)?; 23 | 24 | // Create an instance 25 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 26 | 27 | const TEST_COLLECTION_NAME: &str = "test-batch-write"; 28 | 29 | println!("Populating a test collection"); 30 | let batch_writer = db.create_simple_batch_writer().await?; 31 | 32 | let mut current_batch = batch_writer.new_batch(); 33 | 34 | for idx in 0..500 { 35 | let my_struct = MyTestStructure { 36 | some_id: format!("test-{}", idx), 37 | some_string: "Test".to_string(), 38 | created_at: Utc::now(), 39 | }; 40 | 41 | db.fluent() 42 | .update() 43 | .in_col(TEST_COLLECTION_NAME) 44 | .document_id(&my_struct.some_id) 45 | .object(&my_struct) 46 | .add_to_batch(&mut current_batch)?; 47 | 48 | if idx % 100 == 0 { 49 | let response = current_batch.write().await?; 50 | current_batch = batch_writer.new_batch(); 51 | println!("{:?}", response); 52 | } 53 | } 54 | 55 | Ok(()) 56 | } 57 | -------------------------------------------------------------------------------- /examples/batch-write-streaming.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use firestore::*; 3 | use futures::TryStreamExt; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | pub fn config_env_var(name: &str) -> Result { 7 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 8 | } 9 | 10 | #[derive(Debug, Clone, Deserialize, Serialize)] 11 | struct MyTestStructure { 12 | some_id: String, 13 | some_string: String, 14 | created_at: DateTime, 15 | } 16 | 17 | #[tokio::main] 18 | async fn main() -> Result<(), Box> { 19 | // Logging with debug enabled 20 | let subscriber = tracing_subscriber::fmt() 21 | .with_env_filter("firestore=debug") 22 | .finish(); 23 | tracing::subscriber::set_global_default(subscriber)?; 24 | 25 | // Create an instance 26 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 27 | 28 | const TEST_COLLECTION_NAME: &str = "test-batch-write"; 29 | 30 | println!("Populating a test collection"); 31 | let (batch_writer, mut batch_results_reader) = db.create_streaming_batch_writer().await?; 32 | 33 | let response_thread = tokio::spawn(async move { 34 | while let Ok(Some(response)) = batch_results_reader.try_next().await { 35 | println!("{:?}", response); 36 | } 37 | }); 38 | 39 | let mut current_batch = batch_writer.new_batch(); 40 | for idx in 0..10000 { 41 | let my_struct = MyTestStructure { 42 | some_id: format!("test-{}", idx), 43 | some_string: "Test".to_string(), 44 | created_at: Utc::now(), 45 | }; 46 | 47 | db.fluent() 48 | .update() 49 | .in_col(TEST_COLLECTION_NAME) 50 | .document_id(&my_struct.some_id) 51 | .object(&my_struct) 52 | .add_to_batch(&mut current_batch)?; 53 | 54 | if idx % 100 == 0 { 55 | current_batch.write().await?; 56 | current_batch = batch_writer.new_batch(); 57 | } 58 | } 59 | 60 | println!("Finishing..."); 61 | batch_writer.finish().await; 62 | let _ = tokio::join!(response_thread); 63 | 64 | Ok(()) 65 | } 66 | -------------------------------------------------------------------------------- /examples/caching_memory_collections.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use firestore::*; 3 | use futures::stream::BoxStream; 4 | use futures::TryStreamExt; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | pub fn config_env_var(name: &str) -> Result { 8 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 9 | } 10 | 11 | // Example structure to play with 12 | #[derive(Debug, Clone, Deserialize, Serialize)] 13 | struct MyTestStructure { 14 | some_id: String, 15 | some_string: String, 16 | one_more_string: String, 17 | some_num: u64, 18 | created_at: DateTime, 19 | } 20 | 21 | #[tokio::main] 22 | async fn main() -> Result<(), Box> { 23 | // Logging with debug enabled 24 | let subscriber = tracing_subscriber::fmt() 25 | .with_env_filter("firestore=debug") 26 | .finish(); 27 | tracing::subscriber::set_global_default(subscriber)?; 28 | 29 | // Create an instance 30 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 31 | 32 | const TEST_COLLECTION_NAME: &'static str = "test-caching"; 33 | 34 | let mut cache = FirestoreCache::new( 35 | "example-mem-cache".into(), 36 | &db, 37 | FirestoreMemoryCacheBackend::new( 38 | FirestoreCacheConfiguration::new().add_collection_config( 39 | &db, 40 | FirestoreCacheCollectionConfiguration::new( 41 | TEST_COLLECTION_NAME, 42 | FirestoreListenerTarget::new(1000), 43 | FirestoreCacheCollectionLoadMode::PreloadNone, 44 | ), 45 | ), 46 | )?, 47 | FirestoreMemListenStateStorage::new(), 48 | ) 49 | .await?; 50 | 51 | cache.load().await?; 52 | 53 | if db 54 | .fluent() 55 | .select() 56 | .by_id_in(TEST_COLLECTION_NAME) 57 | .one("test-0") 58 | .await? 59 | .is_none() 60 | { 61 | println!("Populating a test collection"); 62 | let batch_writer = db.create_simple_batch_writer().await?; 63 | let mut current_batch = batch_writer.new_batch(); 64 | 65 | for i in 0..500 { 66 | let my_struct = MyTestStructure { 67 | some_id: format!("test-{}", i), 68 | some_string: "Test".to_string(), 69 | one_more_string: "Test2".to_string(), 70 | some_num: i, 71 | created_at: Utc::now(), 72 | }; 73 | 74 | // Let's insert some data 75 | db.fluent() 76 | .update() 77 | .in_col(TEST_COLLECTION_NAME) 78 | .document_id(&my_struct.some_id) 79 | .object(&my_struct) 80 | .add_to_batch(&mut current_batch)?; 81 | } 82 | current_batch.write().await?; 83 | } 84 | 85 | println!("Getting by id only from cache - won't exist"); 86 | let my_struct0: Option = db 87 | .read_cached_only(&cache) 88 | .fluent() 89 | .select() 90 | .by_id_in(TEST_COLLECTION_NAME) 91 | .obj() 92 | .one("test-1") 93 | .await?; 94 | 95 | println!("{:?}", my_struct0); 96 | 97 | println!("Getting by id"); 98 | let my_struct1: Option = db 99 | .read_through_cache(&cache) 100 | .fluent() 101 | .select() 102 | .by_id_in(TEST_COLLECTION_NAME) 103 | .obj() 104 | .one("test-1") 105 | .await?; 106 | 107 | println!("{:?}", my_struct1); 108 | 109 | println!("Getting by id from cache now"); 110 | let my_struct2: Option = db 111 | .read_through_cache(&cache) 112 | .fluent() 113 | .select() 114 | .by_id_in(TEST_COLLECTION_NAME) 115 | .obj() 116 | .one("test-1") 117 | .await?; 118 | 119 | println!("{:?}", my_struct2); 120 | 121 | println!("Getting batch by ids"); 122 | let cached_db = db.read_through_cache(&cache); 123 | 124 | let my_struct1_stream: BoxStream)>> = 125 | cached_db 126 | .fluent() 127 | .select() 128 | .by_id_in(TEST_COLLECTION_NAME) 129 | .obj() 130 | .batch_with_errors(["test-1", "test-2"]) 131 | .await?; 132 | 133 | let my_structs1 = my_struct1_stream.try_collect::>().await?; 134 | println!("{:?}", my_structs1); 135 | 136 | // Now from cache 137 | let my_struct2_stream: BoxStream)>> = 138 | cached_db 139 | .fluent() 140 | .select() 141 | .by_id_in(TEST_COLLECTION_NAME) 142 | .obj() 143 | .batch_with_errors(["test-1", "test-2"]) 144 | .await?; 145 | 146 | let my_structs2 = my_struct2_stream.try_collect::>().await?; 147 | println!("{:?}", my_structs2); 148 | 149 | // List from cache 150 | let cached_db = db.read_cached_only(&cache); 151 | let all_items_stream = cached_db 152 | .fluent() 153 | .list() 154 | .from(TEST_COLLECTION_NAME) 155 | .obj::() 156 | .stream_all_with_errors() 157 | .await?; 158 | 159 | let listed_items = all_items_stream.try_collect::>().await?; 160 | println!("{:?}", listed_items.len()); 161 | 162 | // Query from cache 163 | let all_items_stream = cached_db 164 | .fluent() 165 | .select() 166 | .from(TEST_COLLECTION_NAME) 167 | .filter(|q| { 168 | q.for_all( 169 | q.field(path!(MyTestStructure::some_num)) 170 | .greater_than_or_equal(2), 171 | ) 172 | }) 173 | .obj::() 174 | .stream_query_with_errors() 175 | .await?; 176 | 177 | let queried_items = all_items_stream.try_collect::>().await?; 178 | println!("{:?}", queried_items.len()); 179 | 180 | cache.shutdown().await?; 181 | 182 | Ok(()) 183 | } 184 | -------------------------------------------------------------------------------- /examples/caching_persistent_collections.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use firestore::*; 3 | use futures::stream::BoxStream; 4 | use futures::TryStreamExt; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | pub fn config_env_var(name: &str) -> Result { 8 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 9 | } 10 | 11 | // Example structure to play with 12 | #[derive(Debug, Clone, Deserialize, Serialize)] 13 | struct MyTestStructure { 14 | some_id: String, 15 | some_string: String, 16 | one_more_string: String, 17 | some_num: u64, 18 | created_at: DateTime, 19 | } 20 | 21 | #[tokio::main] 22 | async fn main() -> Result<(), Box> { 23 | // Logging with debug enabled 24 | let subscriber = tracing_subscriber::fmt() 25 | .with_env_filter("firestore=debug") 26 | .finish(); 27 | tracing::subscriber::set_global_default(subscriber)?; 28 | 29 | // Create an instance 30 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 31 | 32 | const TEST_COLLECTION_NAME: &'static str = "test-caching"; 33 | 34 | let mut cache = FirestoreCache::new( 35 | "example-persistent-cache".into(), 36 | &db, 37 | FirestorePersistentCacheBackend::new( 38 | FirestoreCacheConfiguration::new().add_collection_config( 39 | &db, 40 | FirestoreCacheCollectionConfiguration::new( 41 | TEST_COLLECTION_NAME, 42 | FirestoreListenerTarget::new(1000), 43 | FirestoreCacheCollectionLoadMode::PreloadAllIfEmpty, 44 | ), 45 | ), 46 | )?, 47 | FirestoreTempFilesListenStateStorage::new(), 48 | ) 49 | .await?; 50 | 51 | cache.load().await?; 52 | 53 | if db 54 | .fluent() 55 | .select() 56 | .by_id_in(TEST_COLLECTION_NAME) 57 | .one("test-0") 58 | .await? 59 | .is_none() 60 | { 61 | println!("Populating a test collection"); 62 | let batch_writer = db.create_simple_batch_writer().await?; 63 | let mut current_batch = batch_writer.new_batch(); 64 | 65 | for i in 0..500 { 66 | let my_struct = MyTestStructure { 67 | some_id: format!("test-{}", i), 68 | some_string: "Test".to_string(), 69 | one_more_string: "Test2".to_string(), 70 | some_num: i, 71 | created_at: Utc::now(), 72 | }; 73 | 74 | // Let's insert some data 75 | db.fluent() 76 | .update() 77 | .in_col(TEST_COLLECTION_NAME) 78 | .document_id(&my_struct.some_id) 79 | .object(&my_struct) 80 | .add_to_batch(&mut current_batch)?; 81 | } 82 | current_batch.write().await?; 83 | } 84 | 85 | println!("Getting by id only from cache"); 86 | let my_struct0: Option = db 87 | .read_cached_only(&cache) 88 | .fluent() 89 | .select() 90 | .by_id_in(TEST_COLLECTION_NAME) 91 | .obj() 92 | .one("test-1") 93 | .await?; 94 | 95 | println!("{:?}", my_struct0); 96 | 97 | println!("Getting by id"); 98 | let my_struct1: Option = db 99 | .read_through_cache(&cache) 100 | .fluent() 101 | .select() 102 | .by_id_in(TEST_COLLECTION_NAME) 103 | .obj() 104 | .one("test-1") 105 | .await?; 106 | 107 | println!("{:?}", my_struct1); 108 | 109 | println!("Getting by id from cache now"); 110 | let my_struct2: Option = db 111 | .read_through_cache(&cache) 112 | .fluent() 113 | .select() 114 | .by_id_in(TEST_COLLECTION_NAME) 115 | .obj() 116 | .one("test-1") 117 | .await?; 118 | 119 | println!("{:?}", my_struct2); 120 | 121 | println!("Getting batch by ids"); 122 | let cached_db = db.read_through_cache(&cache); 123 | 124 | let my_struct1_stream: BoxStream)>> = 125 | cached_db 126 | .fluent() 127 | .select() 128 | .by_id_in(TEST_COLLECTION_NAME) 129 | .obj() 130 | .batch_with_errors(["test-1", "test-2"]) 131 | .await?; 132 | 133 | let my_structs1 = my_struct1_stream.try_collect::>().await?; 134 | println!("{:?}", my_structs1); 135 | 136 | // Now from cache 137 | let my_struct2_stream: BoxStream)>> = 138 | cached_db 139 | .fluent() 140 | .select() 141 | .by_id_in(TEST_COLLECTION_NAME) 142 | .obj() 143 | .batch_with_errors(["test-1", "test-2"]) 144 | .await?; 145 | 146 | let my_structs2 = my_struct2_stream.try_collect::>().await?; 147 | println!("{:?}", my_structs2); 148 | 149 | // List from cache 150 | let cached_db = db.read_cached_only(&cache); 151 | let all_items_stream = cached_db 152 | .fluent() 153 | .list() 154 | .from(TEST_COLLECTION_NAME) 155 | .obj::() 156 | .stream_all_with_errors() 157 | .await?; 158 | 159 | let listed_items = all_items_stream.try_collect::>().await?; 160 | println!("{:?}", listed_items.len()); 161 | 162 | // Query from cache 163 | let all_items_stream = cached_db 164 | .fluent() 165 | .select() 166 | .from(TEST_COLLECTION_NAME) 167 | .filter(|q| { 168 | q.for_all( 169 | q.field(path!(MyTestStructure::some_num)) 170 | .greater_than_or_equal(250), 171 | ) 172 | }) 173 | .order_by([( 174 | path!(MyTestStructure::some_num), 175 | FirestoreQueryDirection::Ascending, 176 | )]) 177 | .obj::() 178 | .stream_query_with_errors() 179 | .await?; 180 | 181 | let queried_items = all_items_stream.try_collect::>().await?; 182 | println!( 183 | "{:?} {:?}...", 184 | queried_items.len(), 185 | queried_items.iter().take(5).collect::>() 186 | ); 187 | 188 | cache.shutdown().await?; 189 | 190 | Ok(()) 191 | } 192 | -------------------------------------------------------------------------------- /examples/camel-case.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use firestore::*; 3 | use futures::stream::BoxStream; 4 | use futures::StreamExt; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | pub fn config_env_var(name: &str) -> Result { 8 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 9 | } 10 | 11 | // Example structure to play with 12 | #[derive(Debug, Clone, Deserialize, Serialize)] 13 | #[serde(rename_all = "camelCase")] 14 | struct MyTestStructure { 15 | some_id: String, 16 | some_string: String, 17 | one_more_string: String, 18 | some_num: u64, 19 | created_at: DateTime, 20 | } 21 | 22 | #[tokio::main] 23 | async fn main() -> Result<(), Box> { 24 | // Logging with debug enabled 25 | let subscriber = tracing_subscriber::fmt() 26 | .with_env_filter("firestore=debug") 27 | .finish(); 28 | tracing::subscriber::set_global_default(subscriber)?; 29 | 30 | // Create an instance 31 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 32 | 33 | const TEST_COLLECTION_NAME: &str = "test-camel-case"; 34 | 35 | println!("Populating a test collection"); 36 | for i in 0..10 { 37 | let my_struct = MyTestStructure { 38 | some_id: format!("test-{}", i), 39 | some_string: "Test".to_string(), 40 | one_more_string: "Test2".to_string(), 41 | some_num: 42 - i, 42 | created_at: Utc::now(), 43 | }; 44 | 45 | // Remove if it already exist 46 | db.fluent() 47 | .delete() 48 | .from(TEST_COLLECTION_NAME) 49 | .document_id(&my_struct.some_id) 50 | .execute() 51 | .await?; 52 | 53 | // Let's insert some data 54 | db.fluent() 55 | .insert() 56 | .into(TEST_COLLECTION_NAME) 57 | .document_id(&my_struct.some_id) 58 | .object(&my_struct) 59 | .execute::<()>() 60 | .await?; 61 | } 62 | 63 | println!("Querying a test collection as a stream using Fluent API"); 64 | 65 | // Query as a stream our data 66 | let object_stream: BoxStream = db 67 | .fluent() 68 | .select() 69 | .fields( 70 | paths_camel_case!(MyTestStructure::{some_id, some_num, some_string, one_more_string, created_at}), 71 | ) 72 | .from(TEST_COLLECTION_NAME) 73 | .filter(|q| { 74 | q.for_all([ 75 | q.field(path_camel_case!(MyTestStructure::some_string)).eq("Test"), 76 | ]) 77 | }) 78 | .obj() 79 | .stream_query() 80 | .await?; 81 | 82 | let as_vec: Vec = object_stream.collect().await; 83 | println!("{:?}", as_vec); 84 | 85 | Ok(()) 86 | } 87 | -------------------------------------------------------------------------------- /examples/consistency-selector.rs: -------------------------------------------------------------------------------- 1 | use firestore::*; 2 | use futures::stream::BoxStream; 3 | use serde::{Deserialize, Serialize}; 4 | use tokio_stream::StreamExt; 5 | 6 | pub fn config_env_var(name: &str) -> Result { 7 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 8 | } 9 | 10 | // Example structure to play with 11 | #[derive(Debug, Clone, Deserialize, Serialize)] 12 | struct MyTestStructure { 13 | some_id: String, 14 | some_string: String, 15 | } 16 | 17 | #[tokio::main] 18 | async fn main() -> Result<(), Box> { 19 | // Logging with debug enabled 20 | let subscriber = tracing_subscriber::fmt() 21 | .with_env_filter("firestore=debug") 22 | .finish(); 23 | tracing::subscriber::set_global_default(subscriber)?; 24 | 25 | // Create an instance 26 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 27 | 28 | const TEST_COLLECTION_NAME: &str = "test"; 29 | 30 | println!("Populating a test collection"); 31 | for i in 0..10 { 32 | let my_struct = MyTestStructure { 33 | some_id: format!("test-{}", i), 34 | some_string: "Test".to_string(), 35 | }; 36 | 37 | // Remove if it already exist 38 | db.fluent() 39 | .delete() 40 | .from(TEST_COLLECTION_NAME) 41 | .document_id(&my_struct.some_id) 42 | .execute() 43 | .await?; 44 | 45 | // Let's insert some data 46 | db.fluent() 47 | .insert() 48 | .into(TEST_COLLECTION_NAME) 49 | .document_id(&my_struct.some_id) 50 | .object(&my_struct) 51 | .execute::<()>() 52 | .await?; 53 | } 54 | 55 | println!("Read only transaction to read the state before changes"); 56 | 57 | let transaction = db 58 | .begin_transaction_with_options( 59 | FirestoreTransactionOptions::new().with_mode(FirestoreTransactionMode::ReadOnly), 60 | ) 61 | .await?; 62 | 63 | // Working with consistency selector for reading when necessary 64 | let cdb = db.clone_with_consistency_selector(FirestoreConsistencySelector::Transaction( 65 | transaction.transaction_id.clone(), 66 | )); 67 | 68 | let consistency_read_test: Option = cdb 69 | .fluent() 70 | .select() 71 | .by_id_in(TEST_COLLECTION_NAME) 72 | .obj() 73 | .one("test-0") 74 | .await?; 75 | 76 | println!("The original one: {:?}", consistency_read_test); 77 | 78 | transaction.commit().await?; 79 | 80 | println!("Listing objects as a stream with updated test-0 and removed test-5"); 81 | // Query as a stream our data 82 | let mut objs_stream: BoxStream = db 83 | .fluent() 84 | .list() 85 | .from(TEST_COLLECTION_NAME) 86 | .order_by([( 87 | path!(MyTestStructure::some_id), 88 | FirestoreQueryDirection::Descending, 89 | )]) 90 | .obj() 91 | .stream_all() 92 | .await?; 93 | 94 | while let Some(object) = objs_stream.next().await { 95 | println!("Object in stream: {:?}", object); 96 | } 97 | 98 | Ok(()) 99 | } 100 | -------------------------------------------------------------------------------- /examples/crud.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use firestore::*; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | pub fn config_env_var(name: &str) -> Result { 6 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 7 | } 8 | 9 | // Example structure to play with 10 | #[derive(Debug, Clone, Deserialize, Serialize)] 11 | struct MyTestStructure { 12 | some_id: String, 13 | some_string: String, 14 | one_more_string: String, 15 | some_num: u64, 16 | created_at: DateTime, 17 | } 18 | 19 | #[tokio::main] 20 | async fn main() -> Result<(), Box> { 21 | // Logging with debug enabled 22 | let subscriber = tracing_subscriber::fmt() 23 | .with_env_filter("firestore=debug") 24 | .finish(); 25 | tracing::subscriber::set_global_default(subscriber)?; 26 | 27 | // Create an instance 28 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 29 | 30 | const TEST_COLLECTION_NAME: &str = "test"; 31 | 32 | let my_struct = MyTestStructure { 33 | some_id: "test-1".to_string(), 34 | some_string: "Test".to_string(), 35 | one_more_string: "Test2".to_string(), 36 | some_num: 41, 37 | created_at: Utc::now(), 38 | }; 39 | 40 | db.fluent() 41 | .delete() 42 | .from(TEST_COLLECTION_NAME) 43 | .document_id(&my_struct.some_id) 44 | .execute() 45 | .await?; 46 | 47 | // A fluent version of create document/object 48 | let object_returned: MyTestStructure = db 49 | .fluent() 50 | .insert() 51 | .into(TEST_COLLECTION_NAME) 52 | .document_id(&my_struct.some_id) 53 | .object(&my_struct) 54 | .execute() 55 | .await?; 56 | 57 | println!("Created {:?}", object_returned); 58 | 59 | // Get by id 60 | let obj_by_id: Option = db 61 | .fluent() 62 | .select() 63 | .by_id_in(TEST_COLLECTION_NAME) 64 | .obj() 65 | .one(&my_struct.some_id) 66 | .await?; 67 | 68 | println!("Get by id {:?}", obj_by_id); 69 | 70 | let object_updated: MyTestStructure = db 71 | .fluent() 72 | .update() 73 | .fields(paths!(MyTestStructure::{some_num, one_more_string})) 74 | .in_col(TEST_COLLECTION_NAME) 75 | .document_id(&my_struct.some_id) 76 | .object(&MyTestStructure { 77 | some_num: my_struct.some_num + 1, 78 | one_more_string: "updated-value".to_string(), 79 | ..my_struct.clone() 80 | }) 81 | .execute() 82 | .await?; 83 | 84 | println!("Updated {:?}", object_updated); 85 | 86 | Ok(()) 87 | } 88 | -------------------------------------------------------------------------------- /examples/database_id_option.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use firestore::*; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | pub fn config_env_var(name: &str) -> Result { 6 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 7 | } 8 | 9 | // Example structure to play with 10 | #[derive(Debug, Clone, Deserialize, Serialize)] 11 | struct MyTestStructure { 12 | some_id: String, 13 | some_string: String, 14 | one_more_string: String, 15 | some_num: u64, 16 | created_at: DateTime, 17 | } 18 | 19 | #[tokio::main] 20 | async fn main() -> Result<(), Box> { 21 | // Logging with debug enabled 22 | let subscriber = tracing_subscriber::fmt() 23 | .with_env_filter("firestore=debug") 24 | .finish(); 25 | tracing::subscriber::set_global_default(subscriber)?; 26 | 27 | // Create an instance 28 | let db = FirestoreDb::with_options( 29 | FirestoreDbOptions::new(config_env_var("PROJECT_ID")?) 30 | .with_database_id(config_env_var("DATABASE_ID")?), 31 | ) 32 | .await?; 33 | 34 | const TEST_COLLECTION_NAME: &str = "test"; 35 | 36 | let my_struct = MyTestStructure { 37 | some_id: "test-1".to_string(), 38 | some_string: "Test".to_string(), 39 | one_more_string: "Test2".to_string(), 40 | some_num: 41, 41 | created_at: Utc::now(), 42 | }; 43 | 44 | db.fluent() 45 | .delete() 46 | .from(TEST_COLLECTION_NAME) 47 | .document_id(&my_struct.some_id) 48 | .execute() 49 | .await?; 50 | 51 | // A fluent version of create document/object 52 | let object_returned: MyTestStructure = db 53 | .fluent() 54 | .insert() 55 | .into(TEST_COLLECTION_NAME) 56 | .document_id(&my_struct.some_id) 57 | .object(&my_struct) 58 | .execute() 59 | .await?; 60 | 61 | println!("Created {:?}", object_returned); 62 | 63 | Ok(()) 64 | } 65 | -------------------------------------------------------------------------------- /examples/document-transform.rs: -------------------------------------------------------------------------------- 1 | use firestore::*; 2 | use futures::stream::BoxStream; 3 | use serde::{Deserialize, Serialize}; 4 | use tokio_stream::StreamExt; 5 | 6 | pub fn config_env_var(name: &str) -> Result { 7 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 8 | } 9 | 10 | // Example structure to play with 11 | #[derive(Debug, Clone, Deserialize, Serialize)] 12 | struct MyTestStructure { 13 | some_id: String, 14 | some_num: i32, 15 | some_string: String, 16 | some_array: Vec, 17 | } 18 | 19 | #[tokio::main] 20 | async fn main() -> Result<(), Box> { 21 | // Logging with debug enabled 22 | let subscriber = tracing_subscriber::fmt() 23 | .with_env_filter("firestore=debug") 24 | .finish(); 25 | tracing::subscriber::set_global_default(subscriber)?; 26 | 27 | // Create an instance 28 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 29 | 30 | const TEST_COLLECTION_NAME: &str = "test-transforms"; 31 | 32 | println!("Populating a test collection"); 33 | for i in 0..10 { 34 | let my_struct = MyTestStructure { 35 | some_id: format!("test-{}", i), 36 | some_num: i, 37 | some_string: "Test".to_string(), 38 | some_array: vec![1, 2, 3], 39 | }; 40 | 41 | // Remove if it already exist 42 | db.fluent() 43 | .delete() 44 | .from(TEST_COLLECTION_NAME) 45 | .document_id(&my_struct.some_id) 46 | .execute() 47 | .await?; 48 | 49 | // Let's insert some data 50 | db.fluent() 51 | .insert() 52 | .into(TEST_COLLECTION_NAME) 53 | .document_id(&my_struct.some_id) 54 | .object(&my_struct) 55 | .execute::<()>() 56 | .await?; 57 | } 58 | 59 | println!("Transaction with transformations"); 60 | 61 | let mut transaction = db.begin_transaction().await?; 62 | 63 | // Only transforms 64 | db.fluent() 65 | .update() 66 | .in_col(TEST_COLLECTION_NAME) 67 | .document_id("test-4") 68 | .transforms(|t| { 69 | t.fields([ 70 | t.field(path!(MyTestStructure::some_num)).increment(10), 71 | t.field(path!(MyTestStructure::some_array)) 72 | .append_missing_elements([4, 5]), 73 | t.field(path!(MyTestStructure::some_array)) 74 | .remove_all_from_array([3]), 75 | ]) 76 | }) 77 | .only_transform() 78 | .add_to_transaction(&mut transaction)?; 79 | 80 | // Transforms with update 81 | db.fluent() 82 | .update() 83 | .fields(paths!(MyTestStructure::{ 84 | some_string 85 | })) 86 | .in_col(TEST_COLLECTION_NAME) 87 | .document_id("test-5") 88 | .object(&MyTestStructure { 89 | some_id: "test-5".to_string(), 90 | some_num: 0, 91 | some_string: "UpdatedTest".to_string(), 92 | some_array: vec![1, 2, 3], 93 | }) 94 | .transforms(|t| { 95 | t.fields([ 96 | t.field(path!(MyTestStructure::some_num)).increment(10), 97 | t.field(path!(MyTestStructure::some_array)) 98 | .append_missing_elements([4, 5]), 99 | t.field(path!(MyTestStructure::some_array)) 100 | .remove_all_from_array([3]), 101 | ]) 102 | }) 103 | .add_to_transaction(&mut transaction)?; 104 | 105 | transaction.commit().await?; 106 | 107 | println!("Listing objects"); 108 | // Query as a stream our data 109 | let mut objs_stream: BoxStream = db 110 | .fluent() 111 | .select() 112 | .from(TEST_COLLECTION_NAME) 113 | .order_by([( 114 | path!(MyTestStructure::some_id), 115 | FirestoreQueryDirection::Descending, 116 | )]) 117 | .obj() 118 | .stream_query() 119 | .await?; 120 | 121 | while let Some(object) = objs_stream.next().await { 122 | println!("Object in stream: {:?}", object); 123 | } 124 | 125 | Ok(()) 126 | } 127 | -------------------------------------------------------------------------------- /examples/dynamic_doc_level_crud.rs: -------------------------------------------------------------------------------- 1 | use chrono::prelude::*; 2 | use firestore::*; 3 | 4 | pub fn config_env_var(name: &str) -> Result { 5 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 6 | } 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<(), Box> { 10 | // Logging with debug enabled 11 | let subscriber = tracing_subscriber::fmt() 12 | .with_env_filter("firestore=debug") 13 | .finish(); 14 | tracing::subscriber::set_global_default(subscriber)?; 15 | 16 | // Create an instance 17 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 18 | 19 | const TEST_COLLECTION_NAME: &str = "test"; 20 | 21 | db.fluent() 22 | .delete() 23 | .from(TEST_COLLECTION_NAME) 24 | .document_id("test-1") 25 | .execute() 26 | .await?; 27 | 28 | let object_returned = db 29 | .fluent() 30 | .insert() 31 | .into(TEST_COLLECTION_NAME) 32 | .document_id("test-1") 33 | .document(FirestoreDb::serialize_map_to_doc( 34 | "", 35 | [ 36 | ("some_id", "some-id-value".into()), 37 | ("some_string", "some-string-value".into()), 38 | ("one_more_string", "another-string-value".into()), 39 | ("some_num", 41.into()), 40 | ( 41 | "embedded_obj", 42 | FirestoreValue::from_map([ 43 | ("inner_some_id", "inner-value".into()), 44 | ("inner_some_string", "inner-value".into()), 45 | ]), 46 | ), 47 | ("created_at", FirestoreTimestamp(Utc::now()).into()), 48 | ], 49 | )?) 50 | .execute() 51 | .await?; 52 | 53 | println!("Created {:?}", object_returned); 54 | 55 | let object_updated = db 56 | .fluent() 57 | .update() 58 | .fields(["some_num", "one_more_string"]) 59 | .in_col(TEST_COLLECTION_NAME) 60 | .document(FirestoreDb::serialize_map_to_doc( 61 | db.parent_path(TEST_COLLECTION_NAME, "test-1")?, 62 | [ 63 | ("one_more_string", "update-string".into()), 64 | ("some_num", 42.into()), 65 | ], 66 | )?) 67 | .execute() 68 | .await?; 69 | 70 | println!("Updated {:?}", object_updated); 71 | 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /examples/explain-query.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use firestore::*; 3 | use futures::stream::BoxStream; 4 | use futures::TryStreamExt; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | pub fn config_env_var(name: &str) -> Result { 8 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 9 | } 10 | 11 | // Example structure to play with 12 | #[derive(Debug, Clone, Deserialize, Serialize)] 13 | struct MyTestStructure { 14 | some_id: String, 15 | some_string: String, 16 | one_more_string: String, 17 | some_num: u64, 18 | created_at: DateTime, 19 | } 20 | 21 | #[tokio::main] 22 | async fn main() -> Result<(), Box> { 23 | // Logging with debug enabled 24 | let subscriber = tracing_subscriber::fmt() 25 | .with_env_filter("firestore=debug") 26 | .finish(); 27 | tracing::subscriber::set_global_default(subscriber)?; 28 | 29 | // Create an instance 30 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 31 | 32 | const TEST_COLLECTION_NAME: &str = "test-query"; 33 | 34 | if db 35 | .fluent() 36 | .select() 37 | .by_id_in(TEST_COLLECTION_NAME) 38 | .one("test-0") 39 | .await? 40 | .is_none() 41 | { 42 | println!("Populating a test collection"); 43 | let batch_writer = db.create_simple_batch_writer().await?; 44 | let mut current_batch = batch_writer.new_batch(); 45 | 46 | for i in 0..500 { 47 | let my_struct = MyTestStructure { 48 | some_id: format!("test-{}", i), 49 | some_string: "Test".to_string(), 50 | one_more_string: "Test2".to_string(), 51 | some_num: i, 52 | created_at: Utc::now(), 53 | }; 54 | 55 | // Let's insert some data 56 | db.fluent() 57 | .update() 58 | .in_col(TEST_COLLECTION_NAME) 59 | .document_id(&my_struct.some_id) 60 | .object(&my_struct) 61 | .add_to_batch(&mut current_batch)?; 62 | } 63 | current_batch.write().await?; 64 | } 65 | 66 | println!("Explain querying for a test collection as a stream using Fluent API"); 67 | 68 | // Query as a stream our data 69 | let object_stream: BoxStream>> = db 70 | .fluent() 71 | .select() 72 | .fields( 73 | paths!(MyTestStructure::{some_id, some_num, some_string, one_more_string, created_at}), 74 | ) 75 | .from(TEST_COLLECTION_NAME) 76 | .filter(|q| { 77 | q.for_all([ 78 | q.field(path!(MyTestStructure::some_num)).is_not_null(), 79 | q.field(path!(MyTestStructure::some_string)).eq("Test"), 80 | Some("Test2") 81 | .and_then(|value| q.field(path!(MyTestStructure::one_more_string)).eq(value)), 82 | ]) 83 | }) 84 | .order_by([( 85 | path!(MyTestStructure::some_num), 86 | FirestoreQueryDirection::Descending, 87 | )]) 88 | .explain() 89 | //.explain_with_options(FirestoreExplainOptions::new().with_analyze(true)) or with analyze 90 | .obj() 91 | .stream_query_with_metadata() 92 | .await?; 93 | 94 | let as_vec: Vec> = object_stream.try_collect().await?; 95 | println!("{:?}", as_vec); 96 | 97 | Ok(()) 98 | } 99 | -------------------------------------------------------------------------------- /examples/generated-document-id.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use firestore::*; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | pub fn config_env_var(name: &str) -> Result { 6 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 7 | } 8 | 9 | // Example structure to play with 10 | #[derive(Debug, Clone, Deserialize, Serialize)] 11 | struct MyTestStructure { 12 | #[serde(alias = "_firestore_id")] 13 | id: Option, 14 | #[serde(alias = "_firestore_created")] 15 | created_at: Option>, 16 | #[serde(alias = "_firestore_updated")] 17 | updated_at: Option>, 18 | test: Option, 19 | some_string: String, 20 | one_more_string: String, 21 | some_num: u64, 22 | } 23 | 24 | #[tokio::main] 25 | async fn main() -> Result<(), Box> { 26 | // Logging with debug enabled 27 | let subscriber = tracing_subscriber::fmt() 28 | .with_env_filter("firestore=debug") 29 | .finish(); 30 | tracing::subscriber::set_global_default(subscriber)?; 31 | 32 | // Create an instance 33 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 34 | 35 | const TEST_COLLECTION_NAME: &str = "test"; 36 | 37 | let my_struct = MyTestStructure { 38 | id: None, 39 | created_at: None, 40 | updated_at: None, 41 | test: Some("tsst".to_string()), 42 | some_string: "Test".to_string(), 43 | one_more_string: "Test2".to_string(), 44 | some_num: 41, 45 | }; 46 | 47 | let object_returned: MyTestStructure = db 48 | .fluent() 49 | .insert() 50 | .into(TEST_COLLECTION_NAME) 51 | .generate_document_id() 52 | .object(&my_struct) 53 | .execute() 54 | .await?; 55 | 56 | println!("Created {:?}", object_returned); 57 | 58 | let generated_id = object_returned.id.unwrap(); 59 | 60 | let object_read: Option = db 61 | .fluent() 62 | .select() 63 | .by_id_in(TEST_COLLECTION_NAME) 64 | .obj() 65 | .one(generated_id) 66 | .await?; 67 | 68 | println!("Read {:?}", object_read); 69 | 70 | Ok(()) 71 | } 72 | -------------------------------------------------------------------------------- /examples/group-query.rs: -------------------------------------------------------------------------------- 1 | use firestore::*; 2 | use futures::stream::BoxStream; 3 | use serde::{Deserialize, Serialize}; 4 | use tokio_stream::StreamExt; 5 | 6 | pub fn config_env_var(name: &str) -> Result { 7 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 8 | } 9 | 10 | #[derive(Debug, Clone, Deserialize, Serialize)] 11 | struct MyParentStructure { 12 | some_id: String, 13 | some_string: String, 14 | } 15 | 16 | #[derive(Debug, Clone, Deserialize, Serialize)] 17 | struct MyChildStructure { 18 | some_id: String, 19 | another_string: String, 20 | } 21 | 22 | #[tokio::main] 23 | async fn main() -> Result<(), Box> { 24 | // Logging with debug enabled 25 | let subscriber = tracing_subscriber::fmt() 26 | .with_env_filter("firestore=debug") 27 | .finish(); 28 | tracing::subscriber::set_global_default(subscriber)?; 29 | 30 | // Create an instance 31 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 32 | 33 | const TEST_PARENT_COLLECTION_NAME: &str = "nested-test"; 34 | const TEST_CHILD_COLLECTION_NAME: &str = "test-childs"; 35 | 36 | println!("Populating parent doc/collection"); 37 | 38 | for parent_idx in 0..5 { 39 | let parent_struct = MyParentStructure { 40 | some_id: format!("test-parent-{}", parent_idx), 41 | some_string: "Test".to_string(), 42 | }; 43 | 44 | // Remove if it already exist 45 | db.fluent() 46 | .delete() 47 | .from(TEST_PARENT_COLLECTION_NAME) 48 | .document_id(&parent_struct.some_id) 49 | .execute() 50 | .await?; 51 | 52 | db.fluent() 53 | .insert() 54 | .into(TEST_PARENT_COLLECTION_NAME) 55 | .document_id(&parent_struct.some_id) 56 | .object(&parent_struct) 57 | .execute::<()>() 58 | .await?; 59 | 60 | for child_idx in 0..3 { 61 | // Creating a child doc 62 | let child_struct = MyChildStructure { 63 | some_id: format!("test-parent{}-child-{}", parent_idx, child_idx), 64 | another_string: "TestChild".to_string(), 65 | }; 66 | 67 | // The doc path where we store our childs 68 | let parent_path = 69 | db.parent_path(TEST_PARENT_COLLECTION_NAME, &parent_struct.some_id)?; 70 | 71 | // Remove child doc if exists 72 | db.fluent() 73 | .delete() 74 | .from(TEST_CHILD_COLLECTION_NAME) 75 | .parent(&parent_path) 76 | .document_id(&child_struct.some_id) 77 | .execute() 78 | .await?; 79 | 80 | db.fluent() 81 | .insert() 82 | .into(TEST_CHILD_COLLECTION_NAME) 83 | .document_id(&child_struct.some_id) 84 | .parent(&parent_path) 85 | .object(&child_struct) 86 | .execute::<()>() 87 | .await?; 88 | } 89 | } 90 | 91 | println!("Query children"); 92 | 93 | let mut objs_stream: BoxStream = db 94 | .fluent() 95 | .select() 96 | .from(TEST_CHILD_COLLECTION_NAME) 97 | //.parent(db.parent_path(TEST_PARENT_COLLECTION_NAME, "test-parent-0")) // if you need to search for only one root you need do disable with_all_descendants below 98 | .all_descendants() 99 | .filter(|q| { 100 | q.for_all([q 101 | .field(path!(MyChildStructure::another_string)) 102 | .eq("TestChild")]) 103 | }) 104 | .obj() 105 | .stream_query() 106 | .await?; 107 | 108 | while let Some(object) = objs_stream.next().await { 109 | println!("Object in stream: {:?}", object); 110 | } 111 | 112 | Ok(()) 113 | } 114 | -------------------------------------------------------------------------------- /examples/latlng.rs: -------------------------------------------------------------------------------- 1 | use firestore::*; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | pub fn config_env_var(name: &str) -> Result { 5 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 6 | } 7 | 8 | // Example structure to play with 9 | #[derive(Debug, Clone, Deserialize, Serialize)] 10 | struct MyTestStructure { 11 | some_id: String, 12 | some_latlng: FirestoreLatLng, 13 | } 14 | 15 | #[tokio::main] 16 | async fn main() -> Result<(), Box> { 17 | // Logging with debug enabled 18 | let subscriber = tracing_subscriber::fmt() 19 | .with_env_filter("firestore=debug") 20 | .finish(); 21 | tracing::subscriber::set_global_default(subscriber)?; 22 | 23 | // Create an instance 24 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 25 | 26 | const TEST_COLLECTION_NAME: &str = "test-latlng"; 27 | 28 | let my_struct = MyTestStructure { 29 | some_id: "test-1".to_string(), 30 | some_latlng: FirestoreLatLng(FirestoreGeoPoint { 31 | latitude: 1.0, 32 | longitude: 2.0, 33 | }), 34 | }; 35 | 36 | db.fluent() 37 | .delete() 38 | .from(TEST_COLLECTION_NAME) 39 | .document_id(&my_struct.some_id) 40 | .execute() 41 | .await?; 42 | 43 | // A fluent version of create document/object 44 | let object_returned: MyTestStructure = db 45 | .fluent() 46 | .insert() 47 | .into(TEST_COLLECTION_NAME) 48 | .document_id(&my_struct.some_id) 49 | .object(&my_struct) 50 | .execute() 51 | .await?; 52 | 53 | println!("Created: {:?}", object_returned); 54 | 55 | // Query our data 56 | let objects1: Vec = db 57 | .fluent() 58 | .select() 59 | .from(TEST_COLLECTION_NAME) 60 | .obj() 61 | .query() 62 | .await?; 63 | 64 | println!("Now in the list: {:?}", objects1); 65 | 66 | Ok(()) 67 | } 68 | -------------------------------------------------------------------------------- /examples/list-collections.rs: -------------------------------------------------------------------------------- 1 | use firestore::*; 2 | use futures::TryStreamExt; 3 | 4 | pub fn config_env_var(name: &str) -> Result { 5 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 6 | } 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<(), Box> { 10 | // Logging with debug enabled 11 | let subscriber = tracing_subscriber::fmt() 12 | .with_env_filter("firestore=debug") 13 | .finish(); 14 | tracing::subscriber::set_global_default(subscriber)?; 15 | 16 | // Create an instance 17 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?) 18 | .await? 19 | .clone(); 20 | 21 | println!("Listing collections as a stream"); 22 | 23 | let collections_stream = db 24 | .fluent() 25 | .list() 26 | .collections() 27 | .stream_all_with_errors() 28 | .await?; 29 | 30 | let collections: Vec = collections_stream.try_collect().await?; 31 | println!("Collections: {:?}", collections); 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /examples/list-docs.rs: -------------------------------------------------------------------------------- 1 | use firestore::*; 2 | use futures::stream::BoxStream; 3 | use serde::{Deserialize, Serialize}; 4 | use tokio_stream::StreamExt; 5 | 6 | pub fn config_env_var(name: &str) -> Result { 7 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 8 | } 9 | 10 | // Example structure to play with 11 | #[derive(Debug, Clone, Deserialize, Serialize)] 12 | struct MyTestStructure { 13 | some_id: String, 14 | some_string: String, 15 | one_more_string: String, 16 | some_num: u64, 17 | } 18 | 19 | #[tokio::main] 20 | async fn main() -> Result<(), Box> { 21 | // Logging with debug enabled 22 | let subscriber = tracing_subscriber::fmt() 23 | .with_env_filter("firestore=debug") 24 | .finish(); 25 | tracing::subscriber::set_global_default(subscriber)?; 26 | 27 | // Create an instance 28 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?) 29 | .await? 30 | .clone(); 31 | 32 | const TEST_COLLECTION_NAME: &str = "test"; 33 | 34 | println!("Populating a test collection"); 35 | for i in 0..10 { 36 | let my_struct = MyTestStructure { 37 | some_id: format!("test-{}", i), 38 | some_string: "Test".to_string(), 39 | one_more_string: "Test2".to_string(), 40 | some_num: 42, 41 | }; 42 | 43 | // Remove if it already exist 44 | db.fluent() 45 | .delete() 46 | .from(TEST_COLLECTION_NAME) 47 | .document_id(&my_struct.some_id) 48 | .execute() 49 | .await?; 50 | 51 | // Let's insert some data 52 | db.fluent() 53 | .insert() 54 | .into(TEST_COLLECTION_NAME) 55 | .document_id(&my_struct.some_id) 56 | .object(&my_struct) 57 | .execute::<()>() 58 | .await?; 59 | } 60 | 61 | println!("Listing objects as a stream"); 62 | // Query as a stream our data 63 | let objs_stream: BoxStream = db 64 | .fluent() 65 | .list() 66 | .from(TEST_COLLECTION_NAME) 67 | .page_size(3) // This is decreased just to show an example of automatic pagination, in the real usage please use bigger figure or don't specify it (default is 100) 68 | .order_by([( 69 | path!(MyTestStructure::some_id), 70 | FirestoreQueryDirection::Descending, 71 | )]) 72 | .obj() 73 | .stream_all() 74 | .await?; 75 | 76 | let as_vec: Vec = objs_stream.collect().await; 77 | println!("{:?}", as_vec); 78 | 79 | Ok(()) 80 | } 81 | -------------------------------------------------------------------------------- /examples/listen-changes.rs: -------------------------------------------------------------------------------- 1 | use chrono::prelude::*; 2 | use firestore::*; 3 | use serde::{Deserialize, Serialize}; 4 | use std::io::Read; 5 | 6 | pub fn config_env_var(name: &str) -> Result { 7 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 8 | } 9 | 10 | // Example structure to play with 11 | #[derive(Debug, Clone, Deserialize, Serialize)] 12 | struct MyTestStructure { 13 | #[serde(alias = "_firestore_id")] 14 | doc_id: Option, 15 | some_id: String, 16 | some_string: String, 17 | some_num: u64, 18 | 19 | #[serde(with = "firestore::serialize_as_timestamp")] 20 | created_at: DateTime, 21 | } 22 | 23 | const TEST_COLLECTION_NAME: &str = "test-listen"; 24 | 25 | // The IDs of targets - must be different for different listener targets/listeners in case you have many instances 26 | const TEST_TARGET_ID_BY_QUERY: FirestoreListenerTarget = FirestoreListenerTarget::new(42_u32); 27 | const TEST_TARGET_ID_BY_DOC_IDS: FirestoreListenerTarget = FirestoreListenerTarget::new(17_u32); 28 | 29 | #[tokio::main] 30 | async fn main() -> Result<(), Box> { 31 | // Logging with debug enabled 32 | let subscriber = tracing_subscriber::fmt() 33 | .with_env_filter("firestore=debug") 34 | .finish(); 35 | tracing::subscriber::set_global_default(subscriber)?; 36 | 37 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?) 38 | .await 39 | .unwrap(); 40 | 41 | let mut listener = db 42 | .create_listener( 43 | FirestoreTempFilesListenStateStorage::new(), // or FirestoreMemListenStateStorage or your own implementation 44 | ) 45 | .await?; 46 | 47 | let my_struct = MyTestStructure { 48 | doc_id: None, 49 | some_id: "test-1".to_string(), 50 | some_string: "test-str".to_string(), 51 | some_num: 42, 52 | created_at: Utc::now(), 53 | }; 54 | 55 | let new_doc: MyTestStructure = db 56 | .fluent() 57 | .insert() 58 | .into(TEST_COLLECTION_NAME) 59 | .generate_document_id() 60 | .object(&my_struct) 61 | .execute() 62 | .await?; 63 | 64 | db.fluent() 65 | .select() 66 | .from(TEST_COLLECTION_NAME) 67 | .listen() 68 | .add_target(TEST_TARGET_ID_BY_QUERY, &mut listener)?; 69 | 70 | db.fluent() 71 | .select() 72 | .by_id_in(TEST_COLLECTION_NAME) 73 | .batch_listen([new_doc.doc_id.clone().expect("Doc must be created before")]) 74 | .add_target(TEST_TARGET_ID_BY_DOC_IDS, &mut listener)?; 75 | 76 | listener 77 | .start(|event| async move { 78 | match event { 79 | FirestoreListenEvent::DocumentChange(ref doc_change) => { 80 | println!("Doc changed: {doc_change:?}"); 81 | 82 | if let Some(doc) = &doc_change.document { 83 | let obj: MyTestStructure = 84 | FirestoreDb::deserialize_doc_to::(doc) 85 | .expect("Deserialized object"); 86 | println!("As object: {obj:?}"); 87 | } 88 | } 89 | _ => { 90 | println!("Received a listen response event to handle: {event:?}"); 91 | } 92 | } 93 | 94 | Ok(()) 95 | }) 96 | .await?; 97 | // Wait any input until we shutdown 98 | println!( 99 | "Waiting any other changes. Try firebase console to change in {} now yourself. New doc created id: {:?}", 100 | TEST_COLLECTION_NAME,new_doc.doc_id 101 | ); 102 | std::io::stdin().read_exact(&mut [0u8; 1])?; 103 | 104 | listener.shutdown().await?; 105 | 106 | Ok(()) 107 | } 108 | -------------------------------------------------------------------------------- /examples/nearest-vector-query.rs: -------------------------------------------------------------------------------- 1 | use firestore::*; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | pub fn config_env_var(name: &str) -> Result { 5 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 6 | } 7 | 8 | // Example structure to play with 9 | #[derive(Debug, Clone, Deserialize, Serialize)] 10 | struct MyTestStructure { 11 | some_id: String, 12 | some_string: String, 13 | some_vec: FirestoreVector, 14 | distance: Option, 15 | } 16 | 17 | #[tokio::main] 18 | async fn main() -> Result<(), Box> { 19 | // Logging with debug enabled 20 | let subscriber = tracing_subscriber::fmt() 21 | .with_env_filter("firestore=debug") 22 | .finish(); 23 | tracing::subscriber::set_global_default(subscriber)?; 24 | 25 | // Create an instance 26 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 27 | 28 | const TEST_COLLECTION_NAME: &str = "test-query-vec"; 29 | 30 | if db 31 | .fluent() 32 | .select() 33 | .by_id_in(TEST_COLLECTION_NAME) 34 | .one("test-0") 35 | .await? 36 | .is_none() 37 | { 38 | println!("Populating a test collection"); 39 | let batch_writer = db.create_simple_batch_writer().await?; 40 | let mut current_batch = batch_writer.new_batch(); 41 | 42 | for i in 0..500 { 43 | let my_struct = MyTestStructure { 44 | some_id: format!("test-{}", i), 45 | some_string: "Test".to_string(), 46 | some_vec: vec![i as f64, (i * 10) as f64, (i * 20) as f64].into(), 47 | distance: None, 48 | }; 49 | 50 | // Let's insert some data 51 | db.fluent() 52 | .update() 53 | .in_col(TEST_COLLECTION_NAME) 54 | .document_id(&my_struct.some_id) 55 | .object(&my_struct) 56 | .add_to_batch(&mut current_batch)?; 57 | } 58 | current_batch.write().await?; 59 | } 60 | 61 | println!("Show sample documents in the test collection"); 62 | let as_vec: Vec = db 63 | .fluent() 64 | .select() 65 | .from(TEST_COLLECTION_NAME) 66 | .limit(3) 67 | .obj() 68 | .query() 69 | .await?; 70 | 71 | println!("Examples: {:?}", as_vec); 72 | 73 | println!("Search for a test collection with a vector closest"); 74 | 75 | let as_vec: Vec = db 76 | .fluent() 77 | .select() 78 | .from(TEST_COLLECTION_NAME) 79 | .find_nearest_with_options( 80 | FirestoreFindNearestOptions::new( 81 | path!(MyTestStructure::some_vec), 82 | vec![0.0_f64, 0.0_f64, 0.0_f64].into(), 83 | FirestoreFindNearestDistanceMeasure::Euclidean, 84 | 5, 85 | ) 86 | .with_distance_result_field(path!(MyTestStructure::distance)), 87 | ) 88 | .obj() 89 | .query() 90 | .await?; 91 | 92 | println!("Found: {:?}", as_vec); 93 | 94 | Ok(()) 95 | } 96 | -------------------------------------------------------------------------------- /examples/nested_collections.rs: -------------------------------------------------------------------------------- 1 | use firestore::*; 2 | use futures::stream::BoxStream; 3 | use serde::{Deserialize, Serialize}; 4 | use tokio_stream::StreamExt; 5 | 6 | pub fn config_env_var(name: &str) -> Result { 7 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 8 | } 9 | 10 | #[derive(Debug, Clone, Deserialize, Serialize)] 11 | struct MyParentStructure { 12 | some_id: String, 13 | some_string: String, 14 | } 15 | 16 | #[derive(Debug, Clone, Deserialize, Serialize)] 17 | struct MyChildStructure { 18 | some_id: String, 19 | another_string: String, 20 | } 21 | 22 | #[tokio::main] 23 | async fn main() -> Result<(), Box> { 24 | // Logging with debug enabled 25 | let subscriber = tracing_subscriber::fmt() 26 | .with_env_filter("firestore=debug") 27 | .finish(); 28 | tracing::subscriber::set_global_default(subscriber)?; 29 | 30 | // Create an instance 31 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 32 | 33 | const TEST_PARENT_COLLECTION_NAME: &str = "nested-test"; 34 | const TEST_CHILD_COLLECTION_NAME: &str = "test-childs"; 35 | 36 | println!("Creating a parent doc/collection"); 37 | 38 | let parent_struct = MyParentStructure { 39 | some_id: "test-parent".to_string(), 40 | some_string: "Test".to_string(), 41 | }; 42 | 43 | // Remove if it already exist 44 | db.fluent() 45 | .delete() 46 | .from(TEST_PARENT_COLLECTION_NAME) 47 | .document_id(&parent_struct.some_id) 48 | .execute() 49 | .await?; 50 | 51 | // Creating a parent doc 52 | db.fluent() 53 | .insert() 54 | .into(TEST_PARENT_COLLECTION_NAME) 55 | .document_id(&parent_struct.some_id) 56 | .object(&parent_struct) 57 | .execute::<()>() 58 | .await?; 59 | 60 | // Creating a child doc 61 | let child_struct = MyChildStructure { 62 | some_id: "test-child".to_string(), 63 | another_string: "TestChild".to_string(), 64 | }; 65 | 66 | // The doc path where we store our childs 67 | let parent_path = db.parent_path(TEST_PARENT_COLLECTION_NAME, parent_struct.some_id)?; 68 | 69 | // Remove child doc if exists 70 | db.fluent() 71 | .delete() 72 | .from(TEST_CHILD_COLLECTION_NAME) 73 | .parent(&parent_path) 74 | .document_id(&child_struct.some_id) 75 | .execute() 76 | .await?; 77 | 78 | // Create a child doc 79 | db.fluent() 80 | .insert() 81 | .into(TEST_CHILD_COLLECTION_NAME) 82 | .document_id(&child_struct.some_id) 83 | .parent(&parent_path) 84 | .object(&child_struct) 85 | .execute::<()>() 86 | .await?; 87 | 88 | println!("Listing all children"); 89 | let list_stream: BoxStream = db 90 | .fluent() 91 | .list() 92 | .from(TEST_CHILD_COLLECTION_NAME) 93 | .parent(&parent_path) 94 | .obj() 95 | .stream_all() 96 | .await?; 97 | 98 | let as_vec: Vec = list_stream.collect().await; 99 | println!("{:?}", as_vec); 100 | 101 | println!("Querying in children"); 102 | let query_stream: BoxStream = db 103 | .fluent() 104 | .select() 105 | .from(TEST_CHILD_COLLECTION_NAME) 106 | .parent(&parent_path) 107 | .filter(|q| { 108 | q.for_all([q 109 | .field(path!(MyChildStructure::another_string)) 110 | .eq("TestChild")]) 111 | }) 112 | .obj() 113 | .stream_query() 114 | .await?; 115 | 116 | let as_vec: Vec = query_stream.collect().await; 117 | println!("{:?}", as_vec); 118 | 119 | Ok(()) 120 | } 121 | -------------------------------------------------------------------------------- /examples/partition-query.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use firestore::*; 3 | use futures::stream::BoxStream; 4 | use futures::TryStreamExt; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | pub fn config_env_var(name: &str) -> Result { 8 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 9 | } 10 | 11 | // Example structure to play with 12 | #[derive(Debug, Clone, Deserialize, Serialize)] 13 | struct MyTestStructure { 14 | some_id: String, 15 | some_string: String, 16 | one_more_string: String, 17 | some_num: u64, 18 | created_at: DateTime, 19 | } 20 | 21 | #[tokio::main] 22 | async fn main() -> Result<(), Box> { 23 | // Logging with debug enabled 24 | let subscriber = tracing_subscriber::fmt() 25 | .with_env_filter("firestore=debug") 26 | .finish(); 27 | tracing::subscriber::set_global_default(subscriber)?; 28 | 29 | // Create an instance 30 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 31 | 32 | const TEST_COLLECTION_NAME: &str = "test-partitions"; 33 | 34 | //println!("Populating a test collection"); 35 | // for i in 0..40000 { 36 | // let my_struct = MyTestStructure { 37 | // some_id: format!("test-{}", i), 38 | // some_string: "Test".to_string(), 39 | // one_more_string: "Test2".to_string(), 40 | // some_num: i, 41 | // created_at: Utc::now(), 42 | // }; 43 | // 44 | // if db 45 | // .fluent() 46 | // .select() 47 | // .by_id_in(TEST_COLLECTION_NAME) 48 | // .one(&my_struct.some_id) 49 | // .await? 50 | // .is_none() 51 | // { 52 | // // Let's insert some data 53 | // db.fluent() 54 | // .insert() 55 | // .into(TEST_COLLECTION_NAME) 56 | // .document_id(&my_struct.some_id) 57 | // .object(&my_struct) 58 | // .execute() 59 | // .await?; 60 | // } 61 | // } 62 | 63 | let partition_stream: BoxStream> = db 64 | .fluent() 65 | .select() 66 | .from(TEST_COLLECTION_NAME) 67 | .obj() 68 | .partition_query() 69 | .parallelism(2) 70 | .page_size(10) 71 | .stream_partitions_with_errors() 72 | .await?; 73 | 74 | let as_vec: Vec<(FirestorePartition, MyTestStructure)> = partition_stream.try_collect().await?; 75 | println!("{}", as_vec.len()); 76 | 77 | Ok(()) 78 | } 79 | -------------------------------------------------------------------------------- /examples/query-with-cursor.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use firestore::*; 3 | use futures::stream::BoxStream; 4 | use futures::StreamExt; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | pub fn config_env_var(name: &str) -> Result { 8 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 9 | } 10 | 11 | // Example structure to play with 12 | #[derive(Debug, Clone, Deserialize, Serialize)] 13 | struct MyTestStructure { 14 | some_id: String, 15 | some_string: String, 16 | one_more_string: String, 17 | some_num: u64, 18 | created_at: DateTime, 19 | } 20 | 21 | #[tokio::main] 22 | async fn main() -> Result<(), Box> { 23 | // Logging with debug enabled 24 | let subscriber = tracing_subscriber::fmt() 25 | .with_env_filter("firestore=debug") 26 | .finish(); 27 | tracing::subscriber::set_global_default(subscriber)?; 28 | 29 | // Create an instance 30 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 31 | 32 | const TEST_COLLECTION_NAME: &str = "test"; 33 | 34 | println!("Populating a test collection"); 35 | for i in 0..10 { 36 | let my_struct = MyTestStructure { 37 | some_id: format!("test-{}", i), 38 | some_string: "Test".to_string(), 39 | one_more_string: "Test2".to_string(), 40 | some_num: 42, 41 | created_at: Utc::now(), 42 | }; 43 | 44 | // Remove if it already exist 45 | db.fluent() 46 | .delete() 47 | .from(TEST_COLLECTION_NAME) 48 | .document_id(&my_struct.some_id) 49 | .execute() 50 | .await?; 51 | 52 | // Let's insert some data 53 | db.fluent() 54 | .insert() 55 | .into(TEST_COLLECTION_NAME) 56 | .document_id(&my_struct.some_id) 57 | .object(&my_struct) 58 | .execute::<()>() 59 | .await?; 60 | } 61 | 62 | println!("Querying a test collection in defined order"); 63 | 64 | // Querying as a stream with errors when needed 65 | let object_stream: BoxStream = db 66 | .fluent() 67 | .select() 68 | .from(TEST_COLLECTION_NAME) 69 | .start_at(FirestoreQueryCursor::BeforeValue(vec!["test-5".into()])) 70 | .order_by([( 71 | path!(MyTestStructure::some_id), 72 | FirestoreQueryDirection::Ascending, 73 | )]) 74 | .obj() 75 | .stream_query() 76 | .await?; 77 | 78 | let as_vec: Vec = object_stream.collect().await; 79 | println!("{:?}", as_vec); 80 | 81 | Ok(()) 82 | } 83 | -------------------------------------------------------------------------------- /examples/query.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use firestore::*; 3 | use futures::stream::BoxStream; 4 | use futures::TryStreamExt; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | pub fn config_env_var(name: &str) -> Result { 8 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 9 | } 10 | 11 | // Example structure to play with 12 | #[derive(Debug, Clone, Deserialize, Serialize)] 13 | struct MyTestStructure { 14 | some_id: String, 15 | some_string: String, 16 | one_more_string: String, 17 | some_num: u64, 18 | created_at: DateTime, 19 | } 20 | 21 | #[tokio::main] 22 | async fn main() -> Result<(), Box> { 23 | // Logging with debug enabled 24 | let subscriber = tracing_subscriber::fmt() 25 | .with_env_filter("firestore=debug") 26 | .finish(); 27 | tracing::subscriber::set_global_default(subscriber)?; 28 | 29 | // Create an instance 30 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 31 | 32 | const TEST_COLLECTION_NAME: &str = "test-query"; 33 | 34 | if db 35 | .fluent() 36 | .select() 37 | .by_id_in(TEST_COLLECTION_NAME) 38 | .one("test-0") 39 | .await? 40 | .is_none() 41 | { 42 | println!("Populating a test collection"); 43 | let batch_writer = db.create_simple_batch_writer().await?; 44 | let mut current_batch = batch_writer.new_batch(); 45 | 46 | for i in 0..500 { 47 | let my_struct = MyTestStructure { 48 | some_id: format!("test-{}", i), 49 | some_string: "Test".to_string(), 50 | one_more_string: "Test2".to_string(), 51 | some_num: i, 52 | created_at: Utc::now(), 53 | }; 54 | 55 | // Let's insert some data 56 | db.fluent() 57 | .update() 58 | .in_col(TEST_COLLECTION_NAME) 59 | .document_id(&my_struct.some_id) 60 | .object(&my_struct) 61 | .add_to_batch(&mut current_batch)?; 62 | } 63 | current_batch.write().await?; 64 | } 65 | 66 | println!("Querying a test collection as a stream using Fluent API"); 67 | 68 | // Simple query into vector 69 | // Query as a stream our data 70 | let as_vec: Vec = db 71 | .fluent() 72 | .select() 73 | .from(TEST_COLLECTION_NAME) 74 | .obj() 75 | .query() 76 | .await?; 77 | 78 | println!("{:?}", as_vec); 79 | 80 | // Query as a stream our data with filters and ordering 81 | let object_stream: BoxStream> = db 82 | .fluent() 83 | .select() 84 | .fields( 85 | paths!(MyTestStructure::{some_id, some_num, some_string, one_more_string, created_at}), 86 | ) 87 | .from(TEST_COLLECTION_NAME) 88 | .filter(|q| { 89 | q.for_all([ 90 | q.field(path!(MyTestStructure::some_num)).is_not_null(), 91 | q.field(path!(MyTestStructure::some_string)).eq("Test"), 92 | Some("Test2") 93 | .and_then(|value| q.field(path!(MyTestStructure::one_more_string)).eq(value)), 94 | ]) 95 | }) 96 | .order_by([( 97 | path!(MyTestStructure::some_num), 98 | FirestoreQueryDirection::Descending, 99 | )]) 100 | .obj() 101 | .stream_query_with_errors() 102 | .await?; 103 | 104 | let as_vec: Vec = object_stream.try_collect().await?; 105 | println!("{:?}", as_vec); 106 | 107 | Ok(()) 108 | } 109 | -------------------------------------------------------------------------------- /examples/read-write-transactions.rs: -------------------------------------------------------------------------------- 1 | use firestore::*; 2 | use futures::stream::FuturesOrdered; 3 | use futures::FutureExt; 4 | use serde::{Deserialize, Serialize}; 5 | use tokio_stream::StreamExt; 6 | 7 | pub fn config_env_var(name: &str) -> Result { 8 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 9 | } 10 | 11 | // Example structure to play with 12 | #[derive(Debug, Clone, Deserialize, Serialize)] 13 | struct MyTestStructure { 14 | test_string: String, 15 | } 16 | 17 | const TEST_COLLECTION_NAME: &str = "test-rw-trans"; 18 | const TEST_DOCUMENT_ID: &str = "test_doc_id"; 19 | 20 | /// Creates a document with a counter set to 0 and then concurrently executes futures for `COUNT_ITERATIONS` iterations. 21 | /// Finally, it reads the document again and verifies that the counter matches the expected number of iterations. 22 | #[tokio::main] 23 | async fn main() -> Result<(), Box> { 24 | // Logging with debug enabled 25 | let subscriber = tracing_subscriber::fmt() 26 | .with_env_filter("firestore=debug") 27 | .finish(); 28 | tracing::subscriber::set_global_default(subscriber)?; 29 | 30 | // Create an instance 31 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 32 | 33 | const COUNT_ITERATIONS: usize = 50; 34 | 35 | println!("Creating initial document..."); 36 | 37 | // Remove if it already exists 38 | db.fluent() 39 | .delete() 40 | .from(TEST_COLLECTION_NAME) 41 | .document_id(TEST_DOCUMENT_ID) 42 | .execute() 43 | .await?; 44 | 45 | // Let's insert some data 46 | let my_struct = MyTestStructure { 47 | test_string: String::new(), 48 | }; 49 | 50 | db.fluent() 51 | .insert() 52 | .into(TEST_COLLECTION_NAME) 53 | .document_id(TEST_DOCUMENT_ID) 54 | .object(&my_struct) 55 | .execute::<()>() 56 | .await?; 57 | 58 | println!("Running transactions..."); 59 | 60 | let mut futures = FuturesOrdered::new(); 61 | 62 | for _ in 0..COUNT_ITERATIONS { 63 | futures.push_back(update_value(&db)); 64 | } 65 | 66 | futures.collect::>().await; 67 | 68 | println!("Testing results..."); 69 | 70 | let test_structure: MyTestStructure = db 71 | .fluent() 72 | .select() 73 | .by_id_in(TEST_COLLECTION_NAME) 74 | .obj() 75 | .one(TEST_DOCUMENT_ID) 76 | .await? 77 | .expect("Missing document"); 78 | 79 | assert_eq!(test_structure.test_string.len(), COUNT_ITERATIONS); 80 | 81 | Ok(()) 82 | } 83 | 84 | async fn update_value(db: &FirestoreDb) -> FirestoreResult<()> { 85 | db.run_transaction(|db, transaction| { 86 | async move { 87 | let mut test_structure: MyTestStructure = db 88 | .fluent() 89 | .select() 90 | .by_id_in(TEST_COLLECTION_NAME) 91 | .obj() 92 | .one(TEST_DOCUMENT_ID) 93 | .await? 94 | .expect("Missing document"); 95 | 96 | // Perform some kind of operation that depends on the state of the document 97 | test_structure.test_string += "a"; 98 | 99 | db.fluent() 100 | .update() 101 | .fields(paths!(MyTestStructure::{ 102 | test_string 103 | })) 104 | .in_col(TEST_COLLECTION_NAME) 105 | .document_id(TEST_DOCUMENT_ID) 106 | .object(&test_structure) 107 | .add_to_transaction(transaction)?; 108 | 109 | Ok(()) 110 | } 111 | .boxed() 112 | }) 113 | .await?; 114 | 115 | Ok(()) 116 | } 117 | -------------------------------------------------------------------------------- /examples/reference.rs: -------------------------------------------------------------------------------- 1 | use firestore::*; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | pub fn config_env_var(name: &str) -> Result { 5 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 6 | } 7 | 8 | // Example structure to play with 9 | #[derive(Debug, Clone, Deserialize, Serialize)] 10 | struct MyTestStructure { 11 | some_id: String, 12 | some_ref: FirestoreReference, 13 | } 14 | 15 | #[tokio::main] 16 | async fn main() -> Result<(), Box> { 17 | // Logging with debug enabled 18 | let subscriber = tracing_subscriber::fmt() 19 | .with_env_filter("firestore=debug") 20 | .finish(); 21 | tracing::subscriber::set_global_default(subscriber)?; 22 | 23 | // Create an instance 24 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 25 | 26 | const TEST_COLLECTION_NAME: &str = "test-reference"; 27 | 28 | let my_struct = MyTestStructure { 29 | some_id: "test-1".to_string(), 30 | some_ref: db.parent_path("test-latlng", "test-1")?.into(), 31 | }; 32 | 33 | db.fluent() 34 | .delete() 35 | .from(TEST_COLLECTION_NAME) 36 | .document_id(&my_struct.some_id) 37 | .execute() 38 | .await?; 39 | 40 | // A fluent version of create document/object 41 | let object_returned: MyTestStructure = db 42 | .fluent() 43 | .insert() 44 | .into(TEST_COLLECTION_NAME) 45 | .document_id(&my_struct.some_id) 46 | .object(&my_struct) 47 | .execute() 48 | .await?; 49 | 50 | println!("Created: {:?}", object_returned); 51 | 52 | // Query our data 53 | let objects1: Vec = db 54 | .fluent() 55 | .select() 56 | .from(TEST_COLLECTION_NAME) 57 | .obj() 58 | .query() 59 | .await?; 60 | 61 | println!("Now in the list: {:?}", objects1); 62 | 63 | let (parent_path, collection_name, document_id) = objects1 64 | .first() 65 | .unwrap() 66 | .some_ref 67 | .split(db.get_documents_path()); 68 | 69 | println!("Document ID: {}", document_id); 70 | println!("Collection name: {:?}", collection_name); 71 | println!("Parent Path: {:?}", parent_path); 72 | 73 | // Read by reference 74 | let object_returned: Option = db 75 | .fluent() 76 | .select() 77 | .by_id_in(&collection_name) 78 | .one(document_id) 79 | .await?; 80 | 81 | println!("Object by reference: {:?}", object_returned); 82 | 83 | Ok(()) 84 | } 85 | -------------------------------------------------------------------------------- /examples/timestamp.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use firestore::*; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | pub fn config_env_var(name: &str) -> Result { 6 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 7 | } 8 | 9 | // Example structure to play with 10 | #[derive(Debug, Clone, Deserialize, Serialize)] 11 | struct MyTestStructure { 12 | some_id: String, 13 | // Using a special attribute to indicate timestamp serialization for Firestore 14 | // (for serde_json it will be still the same, usually String serialization, so you can reuse the models) 15 | #[serde(with = "firestore::serialize_as_timestamp")] 16 | created_at: DateTime, 17 | 18 | // Or you can use a wrapping type 19 | updated_at: Option, 20 | updated_at_always_none: Option, 21 | 22 | // Or one more attribute for optionals 23 | #[serde(default)] 24 | #[serde(with = "firestore::serialize_as_optional_timestamp")] 25 | updated_at_attr: Option>, 26 | 27 | #[serde(default)] 28 | #[serde(with = "firestore::serialize_as_optional_timestamp")] 29 | updated_at_attr_always_none: Option>, 30 | } 31 | 32 | #[tokio::main] 33 | async fn main() -> Result<(), Box> { 34 | // Logging with debug enabled 35 | let subscriber = tracing_subscriber::fmt() 36 | .with_env_filter("firestore=debug") 37 | .finish(); 38 | tracing::subscriber::set_global_default(subscriber)?; 39 | 40 | // Create an instance 41 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 42 | 43 | const TEST_COLLECTION_NAME: &str = "test-ts1"; 44 | 45 | let my_struct = MyTestStructure { 46 | some_id: "test-1".to_string(), 47 | created_at: Utc::now(), 48 | updated_at: Some(Utc::now().into()), 49 | updated_at_always_none: None, 50 | updated_at_attr: Some(Utc::now()), 51 | updated_at_attr_always_none: None, 52 | }; 53 | 54 | db.fluent() 55 | .delete() 56 | .from(TEST_COLLECTION_NAME) 57 | .document_id(&my_struct.some_id) 58 | .execute() 59 | .await?; 60 | 61 | // A fluent version of create document/object 62 | let object_returned: MyTestStructure = db 63 | .fluent() 64 | .insert() 65 | .into(TEST_COLLECTION_NAME) 66 | .document_id(&my_struct.some_id) 67 | .object(&my_struct) 68 | .execute() 69 | .await?; 70 | 71 | println!("Created: {:?}", object_returned); 72 | 73 | // Query our data 74 | let objects1: Vec = db 75 | .fluent() 76 | .select() 77 | .from(TEST_COLLECTION_NAME) 78 | .filter(|q| { 79 | q.for_all([q 80 | .field(path!(MyTestStructure::created_at)) 81 | .less_than_or_equal( 82 | firestore::FirestoreTimestamp(Utc::now()), // Using the wrapping type to indicate serialization without attribute 83 | )]) 84 | }) 85 | .obj() 86 | .query() 87 | .await?; 88 | 89 | println!("Now in the list: {:?}", objects1); 90 | 91 | Ok(()) 92 | } 93 | -------------------------------------------------------------------------------- /examples/token_auth.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use firestore::*; 3 | use futures::stream::BoxStream; 4 | use futures::TryStreamExt; 5 | use serde::{Deserialize, Serialize}; 6 | use std::ops::Add; 7 | 8 | pub fn config_env_var(name: &str) -> Result { 9 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 10 | } 11 | 12 | // Example structure to play with 13 | #[derive(Debug, Clone, Deserialize, Serialize)] 14 | struct MyTestStructure { 15 | some_id: String, 16 | some_string: String, 17 | one_more_string: String, 18 | some_num: u64, 19 | created_at: DateTime, 20 | } 21 | 22 | async fn my_token() -> gcloud_sdk::error::Result { 23 | Ok(gcloud_sdk::Token::new( 24 | "Bearer".to_string(), 25 | config_env_var("TOKEN_VALUE") 26 | .expect("TOKEN_VALUE must be specified") 27 | .into(), 28 | chrono::Utc::now().add(std::time::Duration::from_secs(3600)), 29 | )) 30 | } 31 | 32 | #[tokio::main] 33 | async fn main() -> Result<(), Box> { 34 | // Logging with debug enabled 35 | let subscriber = tracing_subscriber::fmt() 36 | .with_env_filter("firestore=debug") 37 | .finish(); 38 | tracing::subscriber::set_global_default(subscriber)?; 39 | 40 | // Create an instance 41 | let db = FirestoreDb::with_options_token_source( 42 | FirestoreDbOptions::new(config_env_var("PROJECT_ID")?), 43 | gcloud_sdk::GCP_DEFAULT_SCOPES.clone(), 44 | gcloud_sdk::TokenSourceType::ExternalSource(Box::new( 45 | gcloud_sdk::ExternalJwtFunctionSource::new(my_token), 46 | )), 47 | ) 48 | .await?; 49 | 50 | const TEST_COLLECTION_NAME: &str = "test-query"; 51 | 52 | // Query as a stream our data 53 | let object_stream: BoxStream> = db 54 | .fluent() 55 | .select() 56 | .from(TEST_COLLECTION_NAME) 57 | .obj() 58 | .stream_query_with_errors() 59 | .await?; 60 | 61 | let as_vec: Vec = object_stream.try_collect().await?; 62 | println!("{:?}", as_vec); 63 | 64 | Ok(()) 65 | } 66 | -------------------------------------------------------------------------------- /examples/transactions.rs: -------------------------------------------------------------------------------- 1 | use firestore::*; 2 | use futures::stream::BoxStream; 3 | use serde::{Deserialize, Serialize}; 4 | use tokio_stream::StreamExt; 5 | 6 | pub fn config_env_var(name: &str) -> Result { 7 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 8 | } 9 | 10 | // Example structure to play with 11 | #[derive(Debug, Clone, Deserialize, Serialize)] 12 | struct MyTestStructure { 13 | some_id: String, 14 | some_string: String, 15 | } 16 | 17 | #[tokio::main] 18 | async fn main() -> Result<(), Box> { 19 | // Logging with debug enabled 20 | let subscriber = tracing_subscriber::fmt() 21 | .with_env_filter("firestore=debug") 22 | .finish(); 23 | tracing::subscriber::set_global_default(subscriber)?; 24 | 25 | // Create an instance 26 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 27 | 28 | const TEST_COLLECTION_NAME: &str = "test"; 29 | 30 | println!("Populating a test collection"); 31 | for i in 0..10 { 32 | let my_struct = MyTestStructure { 33 | some_id: format!("test-{}", i), 34 | some_string: "Test".to_string(), 35 | }; 36 | 37 | // Remove if it already exist 38 | db.fluent() 39 | .delete() 40 | .from(TEST_COLLECTION_NAME) 41 | .document_id(&my_struct.some_id) 42 | .execute() 43 | .await?; 44 | 45 | // Let's insert some data 46 | db.fluent() 47 | .insert() 48 | .into(TEST_COLLECTION_NAME) 49 | .document_id(&my_struct.some_id) 50 | .object(&my_struct) 51 | .execute::<()>() 52 | .await?; 53 | } 54 | 55 | println!("Transaction update/delete on collection"); 56 | 57 | let mut transaction = db.begin_transaction().await?; 58 | 59 | db.fluent() 60 | .update() 61 | .fields(paths!(MyTestStructure::{ 62 | some_string 63 | })) 64 | .in_col(TEST_COLLECTION_NAME) 65 | .document_id("test-0") 66 | .object(&MyTestStructure { 67 | some_id: "test-0".to_string(), 68 | some_string: "UpdatedTest".to_string(), 69 | }) 70 | .add_to_transaction(&mut transaction)?; 71 | 72 | db.fluent() 73 | .delete() 74 | .from(TEST_COLLECTION_NAME) 75 | .document_id("test-5") 76 | .add_to_transaction(&mut transaction)?; 77 | 78 | transaction.commit().await?; 79 | 80 | println!("Listing objects as a stream with updated test-0 and removed test-5"); 81 | // Query as a stream our data 82 | let mut objs_stream: BoxStream = db 83 | .fluent() 84 | .select() 85 | .from(TEST_COLLECTION_NAME) 86 | .order_by([( 87 | path!(MyTestStructure::some_id), 88 | FirestoreQueryDirection::Descending, 89 | )]) 90 | .obj() 91 | .stream_query() 92 | .await?; 93 | 94 | while let Some(object) = objs_stream.next().await { 95 | println!("Object in stream: {:?}", object); 96 | } 97 | 98 | Ok(()) 99 | } 100 | -------------------------------------------------------------------------------- /examples/update-precondition.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use firestore::*; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | pub fn config_env_var(name: &str) -> Result { 6 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 7 | } 8 | 9 | // Example structure to play with 10 | #[derive(Debug, Clone, Deserialize, Serialize)] 11 | struct MyTestStructure { 12 | some_id: String, 13 | some_string: String, 14 | one_more_string: String, 15 | some_num: u64, 16 | created_at: DateTime, 17 | } 18 | 19 | #[tokio::main] 20 | async fn main() -> Result<(), Box> { 21 | // Logging with debug enabled 22 | let subscriber = tracing_subscriber::fmt() 23 | .with_env_filter("firestore=debug") 24 | .finish(); 25 | tracing::subscriber::set_global_default(subscriber)?; 26 | 27 | // Create an instance 28 | let db = FirestoreDb::new(&config_env_var("PROJECT_ID")?).await?; 29 | 30 | const TEST_COLLECTION_NAME: &str = "test"; 31 | 32 | let my_struct = MyTestStructure { 33 | some_id: "test-1".to_string(), 34 | some_string: "Test".to_string(), 35 | one_more_string: "Test2".to_string(), 36 | some_num: 41, 37 | created_at: Utc::now(), 38 | }; 39 | 40 | let object_updated: MyTestStructure = db 41 | .fluent() 42 | .update() 43 | .fields(paths!(MyTestStructure::{some_num, one_more_string})) 44 | .in_col(TEST_COLLECTION_NAME) 45 | .precondition(FirestoreWritePrecondition::Exists(true)) 46 | .document_id(&my_struct.some_id) 47 | .object(&MyTestStructure { 48 | some_num: my_struct.some_num + 1, 49 | one_more_string: "updated-value".to_string(), 50 | ..my_struct.clone() 51 | }) 52 | .execute() 53 | .await?; 54 | 55 | println!("Updated {:?}", object_updated); 56 | 57 | Ok(()) 58 | } 59 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/cache/backends/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "caching-memory")] 2 | mod memory_backend; 3 | #[cfg(feature = "caching-memory")] 4 | pub use memory_backend::*; 5 | 6 | #[cfg(feature = "caching-persistent")] 7 | mod persistent_backend; 8 | #[cfg(feature = "caching-persistent")] 9 | pub use persistent_backend::*; 10 | -------------------------------------------------------------------------------- /src/cache/configuration.rs: -------------------------------------------------------------------------------- 1 | use crate::{FirestoreDb, FirestoreListenerTarget}; 2 | use std::collections::HashMap; 3 | 4 | #[derive(Clone)] 5 | pub struct FirestoreCacheConfiguration { 6 | pub collections: HashMap, 7 | } 8 | 9 | impl FirestoreCacheConfiguration { 10 | #[inline] 11 | pub fn new() -> Self { 12 | Self { 13 | collections: HashMap::new(), 14 | } 15 | } 16 | 17 | #[inline] 18 | pub fn add_collection_config( 19 | mut self, 20 | db: &FirestoreDb, 21 | config: FirestoreCacheCollectionConfiguration, 22 | ) -> Self { 23 | let collection_path = { 24 | if let Some(ref parent) = config.parent { 25 | format!("{}/{}", parent, config.collection_name) 26 | } else { 27 | format!("{}/{}", db.get_documents_path(), config.collection_name) 28 | } 29 | }; 30 | 31 | self.collections.extend( 32 | [(collection_path, config)] 33 | .into_iter() 34 | .collect::>(), 35 | ); 36 | self 37 | } 38 | } 39 | 40 | #[derive(Debug, Clone)] 41 | pub struct FirestoreCacheCollectionConfiguration { 42 | pub collection_name: String, 43 | pub parent: Option, 44 | pub listener_target: FirestoreListenerTarget, 45 | pub collection_load_mode: FirestoreCacheCollectionLoadMode, 46 | pub indices: Vec, 47 | } 48 | 49 | impl FirestoreCacheCollectionConfiguration { 50 | #[inline] 51 | pub fn new( 52 | collection_name: S, 53 | listener_target: FirestoreListenerTarget, 54 | collection_load_mode: FirestoreCacheCollectionLoadMode, 55 | ) -> Self 56 | where 57 | S: AsRef, 58 | { 59 | Self { 60 | collection_name: collection_name.as_ref().to_string(), 61 | parent: None, 62 | listener_target, 63 | collection_load_mode, 64 | indices: Vec::new(), 65 | } 66 | } 67 | 68 | #[inline] 69 | pub fn with_parent(self, parent: S) -> Self 70 | where 71 | S: AsRef, 72 | { 73 | Self { 74 | parent: Some(parent.as_ref().to_string()), 75 | ..self 76 | } 77 | } 78 | 79 | #[inline] 80 | pub fn with_index(self, index: FirestoreCacheIndexConfiguration) -> Self { 81 | let mut indices = self.indices; 82 | indices.push(index); 83 | Self { indices, ..self } 84 | } 85 | } 86 | 87 | #[derive(Debug, Clone)] 88 | pub enum FirestoreCacheCollectionLoadMode { 89 | PreloadAllDocs, 90 | PreloadAllIfEmpty, 91 | PreloadNone, 92 | } 93 | 94 | #[derive(Debug, Clone)] 95 | pub struct FirestoreCacheIndexConfiguration { 96 | pub fields: Vec, 97 | pub unique: bool, 98 | } 99 | 100 | impl FirestoreCacheIndexConfiguration { 101 | #[inline] 102 | pub fn new(fields: I) -> Self 103 | where 104 | I: IntoIterator, 105 | I::Item: AsRef, 106 | { 107 | Self { 108 | fields: fields.into_iter().map(|s| s.as_ref().to_string()).collect(), 109 | unique: false, 110 | } 111 | } 112 | 113 | #[inline] 114 | pub fn unique(self, value: bool) -> Self { 115 | Self { 116 | unique: value, 117 | ..self 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/cache/options.rs: -------------------------------------------------------------------------------- 1 | use crate::FirestoreListenerParams; 2 | use rsb_derive::Builder; 3 | use rvstruct::ValueStruct; 4 | 5 | #[derive(Clone, Debug, Eq, PartialEq, Hash, ValueStruct)] 6 | pub struct FirestoreCacheName(String); 7 | 8 | #[derive(Debug, Eq, PartialEq, Clone, Builder)] 9 | pub struct FirestoreCacheOptions { 10 | pub name: FirestoreCacheName, 11 | pub listener_params: Option, 12 | } 13 | -------------------------------------------------------------------------------- /src/db/batch_simple_writer.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::*; 2 | use crate::{ 3 | FirestoreBatch, FirestoreBatchWriteResponse, FirestoreBatchWriter, FirestoreDb, 4 | FirestoreResult, FirestoreWriteResult, 5 | }; 6 | use async_trait::async_trait; 7 | use futures::TryFutureExt; 8 | use gcloud_sdk::google::firestore::v1::{BatchWriteRequest, Write}; 9 | use rsb_derive::*; 10 | use std::collections::HashMap; 11 | use tracing::*; 12 | 13 | #[derive(Debug, Eq, PartialEq, Clone, Builder)] 14 | pub struct FirestoreSimpleBatchWriteOptions { 15 | retry_max_elapsed_time: Option, 16 | } 17 | 18 | pub struct FirestoreSimpleBatchWriter { 19 | pub db: FirestoreDb, 20 | pub options: FirestoreSimpleBatchWriteOptions, 21 | pub batch_span: Span, 22 | } 23 | 24 | impl FirestoreSimpleBatchWriter { 25 | pub async fn new( 26 | db: FirestoreDb, 27 | options: FirestoreSimpleBatchWriteOptions, 28 | ) -> FirestoreResult { 29 | let batch_span = span!(Level::DEBUG, "Firestore Batch Write"); 30 | 31 | Ok(Self { 32 | db, 33 | options, 34 | batch_span, 35 | }) 36 | } 37 | 38 | pub fn new_batch(&self) -> FirestoreBatch { 39 | FirestoreBatch::new(&self.db, self) 40 | } 41 | } 42 | 43 | #[async_trait] 44 | impl FirestoreBatchWriter for FirestoreSimpleBatchWriter { 45 | type WriteResult = FirestoreBatchWriteResponse; 46 | 47 | async fn write(&self, writes: Vec) -> FirestoreResult { 48 | let backoff = backoff::ExponentialBackoffBuilder::new() 49 | .with_max_elapsed_time( 50 | self.options 51 | .retry_max_elapsed_time 52 | .map(|v| v.to_std()) 53 | .transpose()?, 54 | ) 55 | .build(); 56 | 57 | let request = BatchWriteRequest { 58 | database: self.db.get_database_path().to_string(), 59 | writes, 60 | labels: HashMap::new(), 61 | }; 62 | 63 | backoff::future::retry(backoff, || { 64 | async { 65 | let response = self 66 | .db 67 | .client() 68 | .get() 69 | .batch_write(request.clone()) 70 | .await 71 | .map_err(FirestoreError::from)?; 72 | 73 | let batch_response = response.into_inner(); 74 | 75 | let write_results: FirestoreResult> = batch_response 76 | .write_results 77 | .into_iter() 78 | .map(|s| s.try_into()) 79 | .collect(); 80 | 81 | Ok(FirestoreBatchWriteResponse::new( 82 | 0, 83 | write_results?, 84 | batch_response.status, 85 | )) 86 | } 87 | .map_err(firestore_err_to_backoff) 88 | }) 89 | .await 90 | } 91 | } 92 | 93 | impl FirestoreDb { 94 | pub async fn create_simple_batch_writer(&self) -> FirestoreResult { 95 | self.create_simple_batch_writer_with_options(FirestoreSimpleBatchWriteOptions::new()) 96 | .await 97 | } 98 | 99 | pub async fn create_simple_batch_writer_with_options( 100 | &self, 101 | options: FirestoreSimpleBatchWriteOptions, 102 | ) -> FirestoreResult { 103 | FirestoreSimpleBatchWriter::new(self.clone(), options).await 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/db/batch_writer.rs: -------------------------------------------------------------------------------- 1 | use crate::db::transaction_ops::{TransformObjectOperation, UpdateObjectOperation}; 2 | use crate::db::DeleteOperation; 3 | use crate::errors::FirestoreError; 4 | use crate::{ 5 | FirestoreDb, FirestoreFieldTransform, FirestoreResult, FirestoreWritePrecondition, 6 | FirestoreWriteResult, 7 | }; 8 | use async_trait::async_trait; 9 | use chrono::{DateTime, Utc}; 10 | use gcloud_sdk::google::firestore::v1::Write; 11 | use gcloud_sdk::google::rpc::Status; 12 | use rsb_derive::*; 13 | use serde::Serialize; 14 | 15 | #[async_trait] 16 | pub trait FirestoreBatchWriter { 17 | type WriteResult; 18 | 19 | async fn write(&self, writes: Vec) -> FirestoreResult; 20 | } 21 | 22 | #[derive(Debug, PartialEq, Clone, Builder)] 23 | pub struct FirestoreBatchWriteResponse { 24 | pub position: u64, 25 | pub write_results: Vec, 26 | pub statuses: Vec, 27 | pub commit_time: Option>, 28 | } 29 | 30 | pub struct FirestoreBatch<'a, W> 31 | where 32 | W: FirestoreBatchWriter, 33 | { 34 | pub db: &'a FirestoreDb, 35 | pub writer: &'a W, 36 | pub writes: Vec, 37 | } 38 | 39 | impl<'a, W> FirestoreBatch<'a, W> 40 | where 41 | W: FirestoreBatchWriter, 42 | { 43 | pub(crate) fn new(db: &'a FirestoreDb, writer: &'a W) -> Self { 44 | Self { 45 | db, 46 | writer, 47 | writes: Vec::new(), 48 | } 49 | } 50 | 51 | #[inline] 52 | pub fn add(&mut self, write: I) -> FirestoreResult<&mut Self> 53 | where 54 | I: TryInto, 55 | { 56 | self.writes.push(write.try_into()?); 57 | Ok(self) 58 | } 59 | 60 | #[inline] 61 | pub async fn write(self) -> FirestoreResult { 62 | self.writer.write(self.writes).await 63 | } 64 | 65 | pub fn update_object( 66 | &mut self, 67 | collection_id: &str, 68 | document_id: S, 69 | obj: &T, 70 | update_only: Option>, 71 | precondition: Option, 72 | update_transforms: Vec, 73 | ) -> FirestoreResult<&mut Self> 74 | where 75 | T: Serialize + Sync + Send, 76 | S: AsRef, 77 | { 78 | self.update_object_at( 79 | self.db.get_documents_path(), 80 | collection_id, 81 | document_id, 82 | obj, 83 | update_only, 84 | precondition, 85 | update_transforms, 86 | ) 87 | } 88 | 89 | pub fn update_object_at( 90 | &mut self, 91 | parent: &str, 92 | collection_id: &str, 93 | document_id: S, 94 | obj: &T, 95 | update_only: Option>, 96 | precondition: Option, 97 | update_transforms: Vec, 98 | ) -> FirestoreResult<&mut Self> 99 | where 100 | T: Serialize + Sync + Send, 101 | S: AsRef, 102 | { 103 | self.add(UpdateObjectOperation { 104 | parent: parent.to_string(), 105 | collection_id: collection_id.to_string(), 106 | document_id, 107 | obj, 108 | update_only, 109 | precondition, 110 | update_transforms, 111 | }) 112 | } 113 | 114 | pub fn delete_by_id( 115 | &mut self, 116 | collection_id: &str, 117 | document_id: S, 118 | precondition: Option, 119 | ) -> FirestoreResult<&mut Self> 120 | where 121 | S: AsRef, 122 | { 123 | self.delete_by_id_at( 124 | self.db.get_documents_path(), 125 | collection_id, 126 | document_id, 127 | precondition, 128 | ) 129 | } 130 | 131 | pub fn delete_by_id_at( 132 | &mut self, 133 | parent: &str, 134 | collection_id: &str, 135 | document_id: S, 136 | precondition: Option, 137 | ) -> FirestoreResult<&mut Self> 138 | where 139 | S: AsRef, 140 | { 141 | self.add(DeleteOperation { 142 | parent: parent.to_string(), 143 | collection_id: collection_id.to_string(), 144 | document_id, 145 | precondition, 146 | }) 147 | } 148 | 149 | pub fn transform( 150 | &mut self, 151 | collection_id: &str, 152 | document_id: S, 153 | precondition: Option, 154 | transforms: Vec, 155 | ) -> FirestoreResult<&mut Self> 156 | where 157 | S: AsRef, 158 | { 159 | self.transform_at( 160 | self.db.get_documents_path(), 161 | collection_id, 162 | document_id, 163 | precondition, 164 | transforms, 165 | ) 166 | } 167 | 168 | pub fn transform_at( 169 | &mut self, 170 | parent: &str, 171 | collection_id: &str, 172 | document_id: S, 173 | precondition: Option, 174 | transforms: Vec, 175 | ) -> FirestoreResult<&mut Self> 176 | where 177 | S: AsRef, 178 | { 179 | self.add(TransformObjectOperation { 180 | parent: parent.to_string(), 181 | collection_id: collection_id.to_string(), 182 | document_id, 183 | precondition, 184 | transforms, 185 | }) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/db/create.rs: -------------------------------------------------------------------------------- 1 | use crate::{FirestoreDb, FirestoreResult}; 2 | use async_trait::async_trait; 3 | use chrono::{DateTime, Utc}; 4 | use gcloud_sdk::google::firestore::v1::*; 5 | use serde::{Deserialize, Serialize}; 6 | use tracing::*; 7 | 8 | #[async_trait] 9 | pub trait FirestoreCreateSupport { 10 | async fn create_doc( 11 | &self, 12 | collection_id: &str, 13 | document_id: Option, 14 | input_doc: Document, 15 | return_only_fields: Option>, 16 | ) -> FirestoreResult 17 | where 18 | S: AsRef + Send; 19 | 20 | async fn create_doc_at( 21 | &self, 22 | parent: &str, 23 | collection_id: &str, 24 | document_id: Option, 25 | input_doc: Document, 26 | return_only_fields: Option>, 27 | ) -> FirestoreResult 28 | where 29 | S: AsRef + Send; 30 | 31 | async fn create_obj( 32 | &self, 33 | collection_id: &str, 34 | document_id: Option, 35 | obj: &I, 36 | return_only_fields: Option>, 37 | ) -> FirestoreResult 38 | where 39 | I: Serialize + Sync + Send, 40 | for<'de> O: Deserialize<'de>, 41 | S: AsRef + Send; 42 | 43 | async fn create_obj_at( 44 | &self, 45 | parent: &str, 46 | collection_id: &str, 47 | document_id: Option, 48 | obj: &I, 49 | return_only_fields: Option>, 50 | ) -> FirestoreResult 51 | where 52 | I: Serialize + Sync + Send, 53 | for<'de> O: Deserialize<'de>, 54 | S: AsRef + Send; 55 | } 56 | 57 | #[async_trait] 58 | impl FirestoreCreateSupport for FirestoreDb { 59 | async fn create_doc( 60 | &self, 61 | collection_id: &str, 62 | document_id: Option, 63 | input_doc: Document, 64 | return_only_fields: Option>, 65 | ) -> FirestoreResult 66 | where 67 | S: AsRef + Send, 68 | { 69 | self.create_doc_at( 70 | self.get_documents_path().as_str(), 71 | collection_id, 72 | document_id, 73 | input_doc, 74 | return_only_fields, 75 | ) 76 | .await 77 | } 78 | 79 | async fn create_doc_at( 80 | &self, 81 | parent: &str, 82 | collection_id: &str, 83 | document_id: Option, 84 | input_doc: Document, 85 | return_only_fields: Option>, 86 | ) -> FirestoreResult 87 | where 88 | S: AsRef + Send, 89 | { 90 | let span = span!( 91 | Level::DEBUG, 92 | "Firestore Create Document", 93 | "/firestore/collection_name" = collection_id, 94 | "/firestore/response_time" = field::Empty, 95 | "/firestore/document_name" = field::Empty, 96 | ); 97 | 98 | let create_document_request = gcloud_sdk::tonic::Request::new(CreateDocumentRequest { 99 | parent: parent.into(), 100 | document_id: document_id 101 | .as_ref() 102 | .map(|id| id.as_ref().to_string()) 103 | .unwrap_or_default(), 104 | mask: return_only_fields.as_ref().map(|masks| DocumentMask { 105 | field_paths: masks.clone(), 106 | }), 107 | collection_id: collection_id.into(), 108 | document: Some(input_doc), 109 | }); 110 | 111 | let begin_query_utc: DateTime = Utc::now(); 112 | 113 | let create_response = self 114 | .client() 115 | .get() 116 | .create_document(create_document_request) 117 | .await?; 118 | 119 | let end_query_utc: DateTime = Utc::now(); 120 | let query_duration = end_query_utc.signed_duration_since(begin_query_utc); 121 | 122 | span.record( 123 | "/firestore/response_time", 124 | query_duration.num_milliseconds(), 125 | ); 126 | 127 | let response_inner = create_response.into_inner(); 128 | 129 | span.record("/firestore/document_name", &response_inner.name); 130 | 131 | span.in_scope(|| { 132 | debug!( 133 | collection_id, 134 | document_id = document_id.as_ref().map(|id| id.as_ref()), 135 | "Created a new document.", 136 | ); 137 | }); 138 | 139 | Ok(response_inner) 140 | } 141 | 142 | async fn create_obj( 143 | &self, 144 | collection_id: &str, 145 | document_id: Option, 146 | obj: &I, 147 | return_only_fields: Option>, 148 | ) -> FirestoreResult 149 | where 150 | I: Serialize + Sync + Send, 151 | for<'de> O: Deserialize<'de>, 152 | S: AsRef + Send, 153 | { 154 | self.create_obj_at( 155 | self.get_documents_path().as_str(), 156 | collection_id, 157 | document_id, 158 | obj, 159 | return_only_fields, 160 | ) 161 | .await 162 | } 163 | 164 | async fn create_obj_at( 165 | &self, 166 | parent: &str, 167 | collection_id: &str, 168 | document_id: Option, 169 | obj: &I, 170 | return_only_fields: Option>, 171 | ) -> FirestoreResult 172 | where 173 | I: Serialize + Sync + Send, 174 | for<'de> O: Deserialize<'de>, 175 | S: AsRef + Send, 176 | { 177 | let input_doc = Self::serialize_to_doc("", obj)?; 178 | 179 | let doc = self 180 | .create_doc_at( 181 | parent, 182 | collection_id, 183 | document_id, 184 | input_doc, 185 | return_only_fields, 186 | ) 187 | .await?; 188 | 189 | Self::deserialize_doc_to(&doc) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/db/delete.rs: -------------------------------------------------------------------------------- 1 | use crate::db::safe_document_path; 2 | use crate::{FirestoreDb, FirestoreResult, FirestoreWritePrecondition}; 3 | use async_trait::async_trait; 4 | use chrono::{DateTime, Utc}; 5 | use gcloud_sdk::google::firestore::v1::*; 6 | use tracing::*; 7 | 8 | #[async_trait] 9 | pub trait FirestoreDeleteSupport { 10 | async fn delete_by_id( 11 | &self, 12 | collection_id: &str, 13 | document_id: S, 14 | precondition: Option, 15 | ) -> FirestoreResult<()> 16 | where 17 | S: AsRef + Send; 18 | 19 | async fn delete_by_id_at( 20 | &self, 21 | parent: &str, 22 | collection_id: &str, 23 | document_id: S, 24 | precondition: Option, 25 | ) -> FirestoreResult<()> 26 | where 27 | S: AsRef + Send; 28 | } 29 | 30 | #[async_trait] 31 | impl FirestoreDeleteSupport for FirestoreDb { 32 | async fn delete_by_id( 33 | &self, 34 | collection_id: &str, 35 | document_id: S, 36 | precondition: Option, 37 | ) -> FirestoreResult<()> 38 | where 39 | S: AsRef + Send, 40 | { 41 | self.delete_by_id_at( 42 | self.get_documents_path().as_str(), 43 | collection_id, 44 | document_id, 45 | precondition, 46 | ) 47 | .await 48 | } 49 | 50 | async fn delete_by_id_at( 51 | &self, 52 | parent: &str, 53 | collection_id: &str, 54 | document_id: S, 55 | precondition: Option, 56 | ) -> FirestoreResult<()> 57 | where 58 | S: AsRef + Send, 59 | { 60 | let document_path = safe_document_path(parent, collection_id, document_id.as_ref())?; 61 | 62 | let span = span!( 63 | Level::DEBUG, 64 | "Firestore Delete Document", 65 | "/firestore/collection_name" = collection_id, 66 | "/firestore/response_time" = field::Empty, 67 | "/firestore/document_name" = document_path.as_str(), 68 | ); 69 | 70 | let request = gcloud_sdk::tonic::Request::new(DeleteDocumentRequest { 71 | name: document_path, 72 | current_document: precondition.map(|cond| cond.try_into()).transpose()?, 73 | }); 74 | 75 | let begin_query_utc: DateTime = Utc::now(); 76 | self.client().get().delete_document(request).await?; 77 | let end_query_utc: DateTime = Utc::now(); 78 | let query_duration = end_query_utc.signed_duration_since(begin_query_utc); 79 | 80 | span.record( 81 | "/firestore/response_time", 82 | query_duration.num_milliseconds(), 83 | ); 84 | 85 | span.in_scope(|| { 86 | debug!( 87 | collection_id, 88 | document_id = document_id.as_ref(), 89 | "Deleted a document.", 90 | ); 91 | }); 92 | 93 | Ok(()) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/db/listen_changes_state_storage.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::AnyBoxedErrResult; 2 | use crate::{FirestoreListenerTarget, FirestoreListenerTargetResumeType, FirestoreListenerToken}; 3 | use async_trait::async_trait; 4 | use rvstruct::ValueStruct; 5 | use std::collections::HashMap; 6 | use std::sync::Arc; 7 | use tokio::sync::RwLock; 8 | use tracing::*; 9 | 10 | #[async_trait] 11 | pub trait FirestoreResumeStateStorage { 12 | async fn read_resume_state( 13 | &self, 14 | target: &FirestoreListenerTarget, 15 | ) -> AnyBoxedErrResult>; 16 | 17 | async fn update_resume_token( 18 | &self, 19 | target: &FirestoreListenerTarget, 20 | token: FirestoreListenerToken, 21 | ) -> AnyBoxedErrResult<()>; 22 | } 23 | 24 | #[derive(Clone, Debug)] 25 | pub struct FirestoreTempFilesListenStateStorage { 26 | temp_dir: Option, 27 | } 28 | 29 | impl FirestoreTempFilesListenStateStorage { 30 | pub fn new() -> Self { 31 | Self { temp_dir: None } 32 | } 33 | 34 | pub fn with_temp_dir>(temp_dir: P) -> Self { 35 | debug!( 36 | directory = ?temp_dir.as_ref(), 37 | "Using temp dir for listen state storage.", 38 | ); 39 | 40 | Self { 41 | temp_dir: Some(temp_dir.as_ref().to_path_buf()), 42 | } 43 | } 44 | 45 | fn get_file_path(&self, target: &FirestoreListenerTarget) -> std::path::PathBuf { 46 | let target_state_file_name = format!("{}.{}.tmp", TOKEN_FILENAME_PREFIX, target.value()); 47 | match &self.temp_dir { 48 | Some(temp_dir) => temp_dir.join(target_state_file_name), 49 | None => std::path::PathBuf::from(target_state_file_name), 50 | } 51 | } 52 | } 53 | 54 | const TOKEN_FILENAME_PREFIX: &str = "firestore-listen-token"; 55 | 56 | #[async_trait] 57 | impl FirestoreResumeStateStorage for FirestoreTempFilesListenStateStorage { 58 | async fn read_resume_state( 59 | &self, 60 | target: &FirestoreListenerTarget, 61 | ) -> Result, Box> 62 | { 63 | let target_state_file_name = self.get_file_path(target); 64 | let token = std::fs::read_to_string(target_state_file_name) 65 | .ok() 66 | .map(|str| { 67 | hex::decode(str) 68 | .map(FirestoreListenerToken::new) 69 | .map(FirestoreListenerTargetResumeType::Token) 70 | .map_err(Box::new) 71 | }) 72 | .transpose()?; 73 | 74 | Ok(token) 75 | } 76 | 77 | async fn update_resume_token( 78 | &self, 79 | target: &FirestoreListenerTarget, 80 | token: FirestoreListenerToken, 81 | ) -> Result<(), Box> { 82 | let target_state_file_name = self.get_file_path(target); 83 | 84 | Ok(std::fs::write( 85 | target_state_file_name, 86 | hex::encode(token.value()), 87 | )?) 88 | } 89 | } 90 | 91 | #[derive(Clone, Debug)] 92 | pub struct FirestoreMemListenStateStorage { 93 | tokens: Arc>>, 94 | } 95 | 96 | impl FirestoreMemListenStateStorage { 97 | pub fn new() -> Self { 98 | Self { 99 | tokens: Arc::new(RwLock::new(HashMap::new())), 100 | } 101 | } 102 | 103 | pub async fn get_token( 104 | &self, 105 | target: &FirestoreListenerTarget, 106 | ) -> Option { 107 | self.tokens.read().await.get(target).cloned() 108 | } 109 | } 110 | 111 | #[async_trait] 112 | impl FirestoreResumeStateStorage for FirestoreMemListenStateStorage { 113 | async fn read_resume_state( 114 | &self, 115 | target: &FirestoreListenerTarget, 116 | ) -> Result, Box> 117 | { 118 | Ok(self 119 | .get_token(target) 120 | .await 121 | .map(FirestoreListenerTargetResumeType::Token)) 122 | } 123 | 124 | async fn update_resume_token( 125 | &self, 126 | target: &FirestoreListenerTarget, 127 | token: FirestoreListenerToken, 128 | ) -> Result<(), Box> { 129 | self.tokens.write().await.insert(target.clone(), token); 130 | Ok(()) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/db/options.rs: -------------------------------------------------------------------------------- 1 | use gcloud_sdk::GoogleEnvironment; 2 | use rsb_derive::Builder; 3 | 4 | /// Configuration options for the [`FirestoreDb`](crate::FirestoreDb) client. 5 | /// 6 | /// This struct allows customization of various aspects of the Firestore client, 7 | /// such as the project ID, database ID, retry behavior, and API endpoint. 8 | /// It uses the `rsb_derive::Builder` to provide a convenient builder pattern 9 | /// for constructing options. 10 | /// 11 | /// # Examples 12 | /// 13 | /// ```rust 14 | /// use firestore::FirestoreDbOptions; 15 | /// 16 | /// let options = FirestoreDbOptions::new("my-gcp-project-id".to_string()) 17 | /// .with_database_id("my-custom-db".to_string()) 18 | /// .with_max_retries(5); 19 | /// 20 | /// // To use the default database ID: 21 | /// let default_db_options = FirestoreDbOptions::new("my-gcp-project-id".to_string()); 22 | /// assert_eq!(default_db_options.database_id, firestore::FIREBASE_DEFAULT_DATABASE_ID); 23 | /// ``` 24 | #[derive(Debug, Eq, PartialEq, Clone, Builder)] 25 | pub struct FirestoreDbOptions { 26 | /// The Google Cloud Project ID that owns the Firestore database. 27 | pub google_project_id: String, 28 | 29 | /// The ID of the Firestore database. Defaults to `"(default)"`. 30 | /// Use [`FIREBASE_DEFAULT_DATABASE_ID`](crate::FIREBASE_DEFAULT_DATABASE_ID) for the default. 31 | #[default = "FIREBASE_DEFAULT_DATABASE_ID.to_string()"] 32 | pub database_id: String, 33 | 34 | /// The maximum number of times to retry a failed operation. Defaults to `3`. 35 | /// Retries are typically applied to transient errors. 36 | #[default = "3"] 37 | pub max_retries: usize, 38 | 39 | /// An optional custom URL for the Firestore API. 40 | /// If `None`, the default Google Firestore API endpoint is used. 41 | /// This can be useful for targeting a Firestore emulator. 42 | /// If the `FIRESTORE_EMULATOR_HOST` environment variable is set, it will 43 | /// typically override this and the default URL. 44 | pub firebase_api_url: Option, 45 | } 46 | 47 | impl FirestoreDbOptions { 48 | /// Attempts to create `FirestoreDbOptions` by detecting the Google Project ID 49 | /// from the environment (e.g., Application Default Credentials or GCE metadata server). 50 | /// 51 | /// If the project ID can be detected, it returns `Some(FirestoreDbOptions)` with 52 | /// default values for other fields. Otherwise, it returns `None`. 53 | /// 54 | /// # Examples 55 | /// 56 | /// ```rust,no_run 57 | /// # async fn run() { 58 | /// use firestore::FirestoreDbOptions; 59 | /// 60 | /// if let Some(options) = FirestoreDbOptions::for_default_project_id().await { 61 | /// // Use options to create a FirestoreDb client 62 | /// println!("Detected project ID: {}", options.google_project_id); 63 | /// } else { 64 | /// println!("Could not detect default project ID."); 65 | /// } 66 | /// # } 67 | /// ``` 68 | pub async fn for_default_project_id() -> Option { 69 | let google_project_id = GoogleEnvironment::detect_google_project_id().await; 70 | 71 | google_project_id.map(FirestoreDbOptions::new) 72 | } 73 | } 74 | 75 | /// The default database ID for Firestore, which is `"(default)"`. 76 | pub const FIREBASE_DEFAULT_DATABASE_ID: &str = "(default)"; 77 | -------------------------------------------------------------------------------- /src/db/parent_path_builder.rs: -------------------------------------------------------------------------------- 1 | use crate::db::safe_document_path; 2 | use crate::{FirestoreReference, FirestoreResult}; 3 | use std::fmt::{Display, Formatter}; 4 | 5 | /// A builder for constructing Firestore document paths, typically for parent documents 6 | /// when dealing with sub-collections. 7 | /// 8 | /// `ParentPathBuilder` allows for fluently creating nested document paths. 9 | /// It starts with an initial document path and allows appending further 10 | /// collection and document ID segments. 11 | /// 12 | /// This is often used to specify the parent document when performing operations 13 | /// on a sub-collection. 14 | /// 15 | /// # Examples 16 | /// 17 | /// ```rust 18 | /// use firestore::{FirestoreDb, FirestoreResult, ParentPathBuilder}; 19 | /// 20 | /// # async fn run() -> FirestoreResult<()> { 21 | /// let db = FirestoreDb::new("my-project").await?; 22 | /// 23 | /// // Path to "my-collection/my-doc" 24 | /// let parent_path = db.parent_path("my-collection", "my-doc")?; 25 | /// assert_eq!(parent_path.to_string(), "projects/my-project/databases/(default)/documents/my-collection/my-doc"); 26 | /// 27 | /// // Path to "my-collection/my-doc/sub-collection/sub-doc" 28 | /// let sub_collection_path = parent_path.at("sub-collection", "sub-doc")?; 29 | /// assert_eq!(sub_collection_path.to_string(), "projects/my-project/databases/(default)/documents/my-collection/my-doc/sub-collection/sub-doc"); 30 | /// # Ok(()) 31 | /// # } 32 | /// ``` 33 | #[derive(Debug, Clone)] 34 | pub struct ParentPathBuilder { 35 | value: String, 36 | } 37 | 38 | impl ParentPathBuilder { 39 | /// Creates a new `ParentPathBuilder` with an initial path. 40 | /// This is typically called internally by [`FirestoreDb::parent_path()`](crate::FirestoreDb::parent_path). 41 | #[inline] 42 | pub(crate) fn new(initial: String) -> Self { 43 | Self { value: initial } 44 | } 45 | 46 | /// Appends a collection name and document ID to the current path. 47 | /// 48 | /// This method extends the existing path with `/collection_name/document_id`. 49 | /// 50 | /// # Arguments 51 | /// * `collection_name`: The name of the collection to append. 52 | /// * `document_id`: The ID of the document within that collection. 53 | /// 54 | /// # Errors 55 | /// Returns [`FirestoreError::InvalidParametersError`](crate::errors::FirestoreError::InvalidParametersError) 56 | /// if the `document_id` is invalid (e.g., contains `/`). 57 | #[inline] 58 | pub fn at(self, collection_name: &str, document_id: S) -> FirestoreResult 59 | where 60 | S: AsRef, 61 | { 62 | Ok(Self::new(safe_document_path( 63 | self.value.as_str(), 64 | collection_name, 65 | document_id.as_ref(), 66 | )?)) 67 | } 68 | } 69 | 70 | impl Display for ParentPathBuilder { 71 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 72 | self.value.fmt(f) 73 | } 74 | } 75 | 76 | impl AsRef for ParentPathBuilder { 77 | fn as_ref(&self) -> &str { 78 | self.value.as_str() 79 | } 80 | } 81 | 82 | impl From for String { 83 | fn from(pb: ParentPathBuilder) -> Self { 84 | pb.value 85 | } 86 | } 87 | 88 | impl<'a> From<&'a ParentPathBuilder> for &'a str { 89 | fn from(pb: &'a ParentPathBuilder) -> &'a str { 90 | pb.value.as_str() 91 | } 92 | } 93 | 94 | impl From for FirestoreReference { 95 | fn from(pb: ParentPathBuilder) -> Self { 96 | FirestoreReference(pb.value) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/db/precondition_models.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::FirestoreError; 2 | use crate::timestamp_utils::to_timestamp; 3 | use chrono::prelude::*; 4 | use gcloud_sdk::google::firestore::v1::Precondition; 5 | 6 | /// A precondition on a document, used for conditional write operations in Firestore. 7 | /// 8 | /// Preconditions allow you to specify conditions that must be met for a write 9 | /// operation (create, update, delete) to succeed. If the precondition is not met, 10 | /// the operation will fail, typically with a `DataConflictError` or similar. 11 | /// ``` 12 | #[derive(Debug, Eq, PartialEq, Clone)] 13 | pub enum FirestoreWritePrecondition { 14 | /// The target document must exist (if `true`) or must not exist (if `false`). 15 | /// 16 | /// - `Exists(true)`: The operation will only succeed if the document already exists. 17 | /// Useful for conditional updates or deletes. 18 | /// - `Exists(false)`: The operation will only succeed if the document does not already exist. 19 | /// Useful for conditional creates (to prevent overwriting). 20 | Exists(bool), 21 | 22 | /// The target document must exist and its `update_time` must match the provided timestamp. 23 | /// 24 | /// This is used for optimistic concurrency control. The operation will only succeed 25 | /// if the document has not been modified since the specified `update_time`. 26 | /// The `DateTime` must be microsecond-aligned, as Firestore timestamps have 27 | /// microsecond precision. 28 | UpdateTime(DateTime), 29 | } 30 | 31 | impl TryInto for FirestoreWritePrecondition { 32 | type Error = FirestoreError; 33 | 34 | fn try_into(self) -> Result { 35 | match self { 36 | FirestoreWritePrecondition::Exists(value) => { 37 | Ok(gcloud_sdk::google::firestore::v1::Precondition { 38 | condition_type: Some( 39 | gcloud_sdk::google::firestore::v1::precondition::ConditionType::Exists( 40 | value, 41 | ), 42 | ), 43 | }) 44 | } 45 | FirestoreWritePrecondition::UpdateTime(value) => { 46 | Ok(gcloud_sdk::google::firestore::v1::Precondition { 47 | condition_type: Some( 48 | gcloud_sdk::google::firestore::v1::precondition::ConditionType::UpdateTime( 49 | to_timestamp(value), 50 | ), 51 | ), 52 | }) 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/db/session_params.rs: -------------------------------------------------------------------------------- 1 | use crate::FirestoreConsistencySelector; 2 | use rsb_derive::*; 3 | 4 | /// Parameters that define the behavior of a Firestore session or a specific set of operations. 5 | /// 6 | /// `FirestoreDbSessionParams` allow for configuring aspects like read consistency 7 | /// (e.g., reading data as of a specific time) and caching behavior for operations 8 | /// performed with a [`FirestoreDb`](crate::FirestoreDb) instance that is associated 9 | /// with these parameters. 10 | /// 11 | /// These parameters can be applied to a `FirestoreDb` instance using methods like 12 | /// [`FirestoreDb::with_session_params()`](crate::FirestoreDb::with_session_params) or 13 | /// [`FirestoreDb::clone_with_session_params()`](crate::FirestoreDb::clone_with_session_params). 14 | /// ``` 15 | #[derive(Clone, Builder)] 16 | pub struct FirestoreDbSessionParams { 17 | /// Specifies the consistency guarantee for read operations. 18 | /// 19 | /// If `None` (the default), strong consistency is used (i.e., the latest version of data is read). 20 | /// Can be set to a [`FirestoreConsistencySelector`] to read data at a specific 21 | /// point in time or within a transaction. 22 | pub consistency_selector: Option, 23 | 24 | /// Defines the caching behavior for this session. 25 | /// Defaults to [`FirestoreDbSessionCacheMode::None`]. 26 | /// 27 | /// This field is only effective if the `caching` feature is enabled. 28 | #[default = "FirestoreDbSessionCacheMode::None"] 29 | pub cache_mode: FirestoreDbSessionCacheMode, 30 | } 31 | 32 | /// Defines the caching mode for Firestore operations within a session. 33 | /// 34 | /// This enum is used in [`FirestoreDbSessionParams`] to control how and if 35 | /// caching is utilized for read operations. 36 | #[derive(Clone)] 37 | pub enum FirestoreDbSessionCacheMode { 38 | /// No caching is performed. All read operations go directly to Firestore. 39 | None, 40 | /// Enables read-through caching. 41 | /// 42 | /// When a read operation is performed: 43 | /// 1. The cache is checked first. 44 | /// 2. If data is found in the cache, it's returned. 45 | /// 3. If data is not in the cache, it's fetched from Firestore, stored in the cache, 46 | /// and then returned. 47 | /// 48 | /// This mode is only available if the `caching` feature is enabled. 49 | #[cfg(feature = "caching")] 50 | ReadThroughCache(FirestoreSharedCacheBackend), 51 | /// Reads exclusively from the cache. 52 | /// 53 | /// When a read operation is performed: 54 | /// 1. The cache is checked. 55 | /// 2. If data is found, it's returned. 56 | /// 3. If data is not found, the operation will typically result in a "not found" 57 | /// status without attempting to fetch from Firestore. 58 | /// 59 | /// This mode is only available if the `caching` feature is enabled. 60 | #[cfg(feature = "caching")] 61 | ReadCachedOnly(FirestoreSharedCacheBackend), 62 | } 63 | 64 | /// A type alias for a thread-safe, shareable Firestore cache backend. 65 | /// 66 | /// This is an `Arc` (Atomically Reference Counted) pointer to a trait object 67 | /// that implements [`FirestoreCacheBackend`](crate::FirestoreCacheBackend). 68 | /// It allows multiple parts of the application or different `FirestoreDb` instances 69 | /// to share the same underlying cache storage. 70 | /// 71 | /// This type is only available if the `caching` feature is enabled. 72 | #[cfg(feature = "caching")] 73 | pub type FirestoreSharedCacheBackend = 74 | std::sync::Arc; 75 | -------------------------------------------------------------------------------- /src/db/transaction_models.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::FirestoreError; 2 | use crate::{FirestoreConsistencySelector, FirestoreWriteResult}; 3 | use chrono::prelude::*; 4 | use chrono::Duration; 5 | use rsb_derive::Builder; 6 | 7 | /// Options for configuring a Firestore transaction. 8 | /// 9 | /// These options control the behavior of a transaction, such as its mode (read-only or read-write) 10 | /// and consistency requirements for read-only transactions. 11 | #[derive(Debug, Eq, PartialEq, Clone, Builder)] 12 | pub struct FirestoreTransactionOptions { 13 | /// The mode of the transaction (e.g., read-only, read-write). 14 | /// Defaults to [`FirestoreTransactionMode::ReadWrite`]. 15 | #[default = "FirestoreTransactionMode::ReadWrite"] 16 | pub mode: FirestoreTransactionMode, 17 | /// An optional maximum duration for the entire transaction, including retries. 18 | /// If set, the transaction will attempt to complete within this duration. 19 | /// If `None`, default retry policies of the underlying gRPC client or Firestore service apply. 20 | pub max_elapsed_time: Option, 21 | } 22 | 23 | impl Default for FirestoreTransactionOptions { 24 | fn default() -> Self { 25 | Self { 26 | mode: FirestoreTransactionMode::ReadWrite, 27 | max_elapsed_time: None, 28 | } 29 | } 30 | } 31 | 32 | impl TryFrom 33 | for gcloud_sdk::google::firestore::v1::TransactionOptions 34 | { 35 | type Error = FirestoreError; 36 | 37 | fn try_from(options: FirestoreTransactionOptions) -> Result { 38 | match options.mode { 39 | FirestoreTransactionMode::ReadOnly => { 40 | Ok(gcloud_sdk::google::firestore::v1::TransactionOptions { 41 | mode: Some( 42 | gcloud_sdk::google::firestore::v1::transaction_options::Mode::ReadOnly( 43 | gcloud_sdk::google::firestore::v1::transaction_options::ReadOnly { 44 | consistency_selector: None, 45 | }, 46 | ), 47 | ), 48 | }) 49 | } 50 | FirestoreTransactionMode::ReadOnlyWithConsistency(ref selector) => { 51 | Ok(gcloud_sdk::google::firestore::v1::TransactionOptions { 52 | mode: Some( 53 | gcloud_sdk::google::firestore::v1::transaction_options::Mode::ReadOnly( 54 | gcloud_sdk::google::firestore::v1::transaction_options::ReadOnly { 55 | consistency_selector: Some(selector.try_into()?), 56 | }, 57 | ), 58 | ), 59 | }) 60 | } 61 | FirestoreTransactionMode::ReadWrite => { 62 | Ok(gcloud_sdk::google::firestore::v1::TransactionOptions { 63 | mode: Some( 64 | gcloud_sdk::google::firestore::v1::transaction_options::Mode::ReadWrite( 65 | gcloud_sdk::google::firestore::v1::transaction_options::ReadWrite { 66 | retry_transaction: vec![], 67 | }, 68 | ), 69 | ), 70 | }) 71 | } 72 | FirestoreTransactionMode::ReadWriteRetry(tid) => { 73 | Ok(gcloud_sdk::google::firestore::v1::TransactionOptions { 74 | mode: Some( 75 | gcloud_sdk::google::firestore::v1::transaction_options::Mode::ReadWrite( 76 | gcloud_sdk::google::firestore::v1::transaction_options::ReadWrite { 77 | retry_transaction: tid, 78 | }, 79 | ), 80 | ), 81 | }) 82 | } 83 | } 84 | } 85 | } 86 | 87 | /// Defines the mode of a Firestore transaction. 88 | #[derive(Debug, Eq, PartialEq, Clone)] 89 | pub enum FirestoreTransactionMode { 90 | /// A read-only transaction. 91 | /// 92 | /// In this mode, only read operations are allowed. The transaction will use 93 | /// strong consistency by default, reading the latest version of data. 94 | ReadOnly, 95 | /// A read-write transaction. 96 | /// 97 | /// This is the default mode. Both read and write operations are allowed. 98 | /// Firestore ensures atomicity for all operations within the transaction. 99 | ReadWrite, 100 | /// A read-only transaction with a specific consistency requirement. 101 | /// 102 | /// Allows specifying how data should be read, for example, at a particular 103 | /// A read-write transaction.src/db/transaction_models.rs:36:28, at a particular 104 | /// point in time using [`FirestoreConsistencySelector::ReadTime`]. 105 | ReadOnlyWithConsistency(FirestoreConsistencySelector), 106 | /// A read-write transaction that attempts to retry a previous transaction. 107 | /// 108 | /// This is used internally by the client when retrying a transaction that 109 | /// failed due to contention or other transient issues. The `FirestoreTransactionId` 110 | /// is the ID of the transaction to retry. 111 | ReadWriteRetry(FirestoreTransactionId), 112 | } 113 | 114 | /// A type alias for Firestore transaction IDs. 115 | /// Transaction IDs are represented as a vector of bytes. 116 | pub type FirestoreTransactionId = Vec; 117 | 118 | /// Represents the response from committing a Firestore transaction. 119 | #[derive(Debug, PartialEq, Clone, Builder)] 120 | pub struct FirestoreTransactionResponse { 121 | /// A list of results for each write operation performed within the transaction. 122 | /// Each [`FirestoreWriteResult`] provides information about a specific write, 123 | /// such as its update time. 124 | pub write_results: Vec, 125 | /// The time at which the transaction was committed. 126 | /// This is `None` if the transaction was read-only or did not involve writes. 127 | pub commit_time: Option>, 128 | } 129 | -------------------------------------------------------------------------------- /src/db/update.rs: -------------------------------------------------------------------------------- 1 | use crate::db::safe_document_path; 2 | use crate::{FirestoreDb, FirestoreResult, FirestoreWritePrecondition}; 3 | use async_trait::async_trait; 4 | use chrono::{DateTime, Utc}; 5 | use gcloud_sdk::google::firestore::v1::*; 6 | use serde::{Deserialize, Serialize}; 7 | use tracing::*; 8 | 9 | #[async_trait] 10 | pub trait FirestoreUpdateSupport { 11 | async fn update_obj( 12 | &self, 13 | collection_id: &str, 14 | document_id: S, 15 | obj: &I, 16 | update_only: Option>, 17 | return_only_fields: Option>, 18 | precondition: Option, 19 | ) -> FirestoreResult 20 | where 21 | I: Serialize + Sync + Send, 22 | for<'de> O: Deserialize<'de>, 23 | S: AsRef + Send; 24 | 25 | async fn update_obj_at( 26 | &self, 27 | parent: &str, 28 | collection_id: &str, 29 | document_id: S, 30 | obj: &I, 31 | update_only: Option>, 32 | return_only_fields: Option>, 33 | precondition: Option, 34 | ) -> FirestoreResult 35 | where 36 | I: Serialize + Sync + Send, 37 | for<'de> O: Deserialize<'de>, 38 | S: AsRef + Send; 39 | 40 | async fn update_doc( 41 | &self, 42 | collection_id: &str, 43 | firestore_doc: Document, 44 | update_only: Option>, 45 | return_only_fields: Option>, 46 | precondition: Option, 47 | ) -> FirestoreResult; 48 | } 49 | 50 | #[async_trait] 51 | impl FirestoreUpdateSupport for FirestoreDb { 52 | async fn update_obj( 53 | &self, 54 | collection_id: &str, 55 | document_id: S, 56 | obj: &I, 57 | update_only: Option>, 58 | return_only_fields: Option>, 59 | precondition: Option, 60 | ) -> FirestoreResult 61 | where 62 | I: Serialize + Sync + Send, 63 | for<'de> O: Deserialize<'de>, 64 | S: AsRef + Send, 65 | { 66 | self.update_obj_at( 67 | self.get_documents_path().as_str(), 68 | collection_id, 69 | document_id, 70 | obj, 71 | update_only, 72 | return_only_fields, 73 | precondition, 74 | ) 75 | .await 76 | } 77 | 78 | async fn update_obj_at( 79 | &self, 80 | parent: &str, 81 | collection_id: &str, 82 | document_id: S, 83 | obj: &I, 84 | update_only: Option>, 85 | return_only_fields: Option>, 86 | precondition: Option, 87 | ) -> FirestoreResult 88 | where 89 | I: Serialize + Sync + Send, 90 | for<'de> O: Deserialize<'de>, 91 | S: AsRef + Send, 92 | { 93 | let firestore_doc = Self::serialize_to_doc( 94 | safe_document_path(parent, collection_id, document_id.as_ref())?.as_str(), 95 | obj, 96 | )?; 97 | 98 | let doc = self 99 | .update_doc( 100 | collection_id, 101 | firestore_doc, 102 | update_only, 103 | return_only_fields, 104 | precondition, 105 | ) 106 | .await?; 107 | 108 | Self::deserialize_doc_to(&doc) 109 | } 110 | 111 | async fn update_doc( 112 | &self, 113 | collection_id: &str, 114 | firestore_doc: Document, 115 | update_only: Option>, 116 | return_only_fields: Option>, 117 | precondition: Option, 118 | ) -> FirestoreResult { 119 | let document_id = firestore_doc.name.clone(); 120 | 121 | let span = span!( 122 | Level::DEBUG, 123 | "Firestore Update Document", 124 | "/firestore/collection_name" = collection_id, 125 | "/firestore/document_name" = document_id, 126 | "/firestore/response_time" = field::Empty, 127 | ); 128 | 129 | let update_document_request = gcloud_sdk::tonic::Request::new(UpdateDocumentRequest { 130 | update_mask: update_only.map({ 131 | |vf| DocumentMask { 132 | field_paths: vf.iter().map(|f| f.to_string()).collect(), 133 | } 134 | }), 135 | document: Some(firestore_doc), 136 | mask: return_only_fields.as_ref().map(|masks| DocumentMask { 137 | field_paths: masks.clone(), 138 | }), 139 | current_document: precondition.map(|cond| cond.try_into()).transpose()?, 140 | }); 141 | 142 | let begin_query_utc: DateTime = Utc::now(); 143 | let update_response = self 144 | .client() 145 | .get() 146 | .update_document(update_document_request) 147 | .await?; 148 | let end_query_utc: DateTime = Utc::now(); 149 | let query_duration = end_query_utc.signed_duration_since(begin_query_utc); 150 | 151 | span.record( 152 | "/firestore/response_time", 153 | query_duration.num_milliseconds(), 154 | ); 155 | 156 | span.in_scope(|| { 157 | debug!(collection_id, document_id, "Updated the document."); 158 | }); 159 | 160 | Ok(update_response.into_inner()) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/firestore_document_functions.rs: -------------------------------------------------------------------------------- 1 | use crate::FirestoreDocument; 2 | use std::collections::HashMap; 3 | 4 | /// Retrieves a field's value from a Firestore document using a dot-separated path. 5 | /// 6 | /// This function allows accessing nested fields within a document's map values. 7 | /// For example, given a document with a field `user` which is a map containing 8 | /// a field `name`, you can retrieve the value of `name` using the path `"user.name"`. 9 | /// 10 | /// Backticks (`) in field paths are automatically removed, as they are sometimes 11 | /// used by the `struct_path` macro for escaping. 12 | /// 13 | /// # Arguments 14 | /// * `doc`: A reference to the [`FirestoreDocument`] to extract the field from. 15 | /// * `field_path`: A dot-separated string representing the path to the desired field. 16 | /// 17 | /// # Returns 18 | /// Returns `Some(&gcloud_sdk::google::firestore::v1::value::ValueType)` if the field 19 | /// is found at the specified path, otherwise `None`. The `ValueType` enum holds the 20 | /// actual typed value (e.g., `StringValue`, `IntegerValue`). 21 | /// 22 | /// # Examples 23 | /// ```rust 24 | /// use firestore::{firestore_doc_get_field_by_path, FirestoreDocument, FirestoreValue}; 25 | /// use gcloud_sdk::google::firestore::v1::MapValue; 26 | /// use std::collections::HashMap; 27 | /// 28 | /// let mut fields = HashMap::new(); 29 | /// let mut user_map_fields = HashMap::new(); 30 | /// user_map_fields.insert("name".to_string(), gcloud_sdk::google::firestore::v1::Value { 31 | /// value_type: Some(gcloud_sdk::google::firestore::v1::value::ValueType::StringValue("Alice".to_string())), 32 | /// }); 33 | /// fields.insert("user".to_string(), gcloud_sdk::google::firestore::v1::Value { 34 | /// value_type: Some(gcloud_sdk::google::firestore::v1::value::ValueType::MapValue(MapValue { fields: user_map_fields })), 35 | /// }); 36 | /// 37 | /// let doc = FirestoreDocument { 38 | /// name: "projects/p/databases/d/documents/c/doc1".to_string(), 39 | /// fields, 40 | /// create_time: None, 41 | /// update_time: None, 42 | /// }; 43 | /// 44 | /// let name_value_type = firestore_doc_get_field_by_path(&doc, "user.name"); 45 | /// assert!(name_value_type.is_some()); 46 | /// if let Some(gcloud_sdk::google::firestore::v1::value::ValueType::StringValue(name)) = name_value_type { 47 | /// assert_eq!(name, "Alice"); 48 | /// } else { 49 | /// panic!("Expected StringValue"); 50 | /// } 51 | /// 52 | /// let non_existent_value = firestore_doc_get_field_by_path(&doc, "user.age"); 53 | /// assert!(non_existent_value.is_none()); 54 | /// ``` 55 | pub fn firestore_doc_get_field_by_path<'d>( 56 | doc: &'d FirestoreDocument, 57 | field_path: &str, 58 | ) -> Option<&'d gcloud_sdk::google::firestore::v1::value::ValueType> { 59 | let field_path: Vec = field_path 60 | .split('.') 61 | .map(|s| s.to_string().replace('`', "")) 62 | .collect(); 63 | firestore_doc_get_field_by_path_arr(&doc.fields, &field_path) 64 | } 65 | 66 | /// Internal helper function to recursively navigate the document fields. 67 | fn firestore_doc_get_field_by_path_arr<'d>( 68 | fields: &'d HashMap, 69 | field_path_arr: &[String], 70 | ) -> Option<&'d gcloud_sdk::google::firestore::v1::value::ValueType> { 71 | field_path_arr.first().and_then(|field_name| { 72 | fields.get(field_name).and_then(|field_value| { 73 | if field_path_arr.len() == 1 { 74 | field_value.value_type.as_ref() 75 | } else { 76 | match field_value.value_type { 77 | Some(gcloud_sdk::google::firestore::v1::value::ValueType::MapValue( 78 | ref map_value, 79 | )) => { 80 | firestore_doc_get_field_by_path_arr(&map_value.fields, &field_path_arr[1..]) 81 | } 82 | _ => None, 83 | } 84 | } 85 | }) 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /src/firestore_serde/mod.rs: -------------------------------------------------------------------------------- 1 | //! Provides custom Serde serializers and deserializers for converting between 2 | //! Rust types and Firestore's native data representation. 3 | //! 4 | //! This module is central to enabling idiomatic Rust struct usage with Firestore. 5 | //! It handles the mapping of Rust types (like `String`, `i64`, `bool`, `Vec`, `HashMap`, 6 | //! and custom structs) to Firestore's `Value` protobuf message, and vice-versa. 7 | //! 8 | //! Key components: 9 | //! - [`FirestoreValueSerializer`](serializer::FirestoreValueSerializer): Implements `serde::Serializer` 10 | //! to convert Rust types into [`FirestoreValue`](crate::FirestoreValue). 11 | //! - [`FirestoreValueDeserializer`](deserializer::FirestoreValueDeserializer): Implements `serde::Deserializer` 12 | //! to convert [`FirestoreValue`](crate::FirestoreValue) back into Rust types. 13 | //! - Helper modules (e.g., `timestamp_serializers`, `latlng_serializers`) provide 14 | //! `#[serde(with = "...")]` compatible functions for specific Firestore types like 15 | //! Timestamps and GeoPoints. 16 | //! 17 | //! The primary public functions re-exported here are: 18 | //! - [`firestore_document_to_serializable`]: Deserializes a Firestore document into a Rust struct. 19 | //! - [`firestore_document_from_serializable`]: Serializes a Rust struct into a Firestore document. 20 | //! - [`firestore_document_from_map`]: Creates a Firestore document from a map of field names to `FirestoreValue`s. 21 | //! 22 | //! Additionally, a generic `From for FirestoreValue where T: serde::Serialize` 23 | //! implementation is provided, allowing easy conversion of any serializable Rust type 24 | //! into a `FirestoreValue`. 25 | 26 | mod deserializer; 27 | mod serializer; 28 | 29 | /// Provides `#[serde(with = "...")]` serializers and deserializers for Firestore Timestamps 30 | /// (converting between `chrono::DateTime` and `google::protobuf::Timestamp`). 31 | mod timestamp_serializers; 32 | pub use timestamp_serializers::*; 33 | 34 | /// Provides `#[serde(with = "...")]` serializers and deserializers for Firestore Null values, 35 | /// particularly for handling `Option` where `None` maps to a NullValue. 36 | mod null_serializers; 37 | pub use null_serializers::*; 38 | 39 | /// Provides `#[serde(with = "...")]` serializers and deserializers for Firestore GeoPoint values 40 | /// (converting between a suitable Rust type like a struct with `latitude` and `longitude` 41 | /// fields and `google::type::LatLng`). 42 | mod latlng_serializers; 43 | pub use latlng_serializers::*; 44 | 45 | /// Provides `#[serde(with = "...")]` serializers and deserializers for Firestore DocumentReference values 46 | /// (converting between `String` or a custom `FirestoreReference` type and Firestore's reference format). 47 | mod reference_serializers; 48 | pub use reference_serializers::*; 49 | 50 | /// Provides `#[serde(with = "...")]` serializers and deserializers for Firestore Vector values. 51 | mod vector_serializers; 52 | pub use vector_serializers::*; 53 | 54 | use crate::FirestoreValue; 55 | use gcloud_sdk::google::firestore::v1::Value; 56 | 57 | pub use deserializer::firestore_document_to_serializable; 58 | pub use serializer::firestore_document_from_map; 59 | pub use serializer::firestore_document_from_serializable; 60 | 61 | /// Generic conversion from any `serde::Serialize` type into a [`FirestoreValue`]. 62 | /// 63 | /// This allows for convenient creation of `FirestoreValue` instances from various Rust types 64 | /// using `.into()`. If serialization fails (which is rare for well-behaved `Serialize` 65 | /// implementations), it defaults to a `FirestoreValue` representing a "null" or empty value. 66 | /// 67 | /// # Examples 68 | /// ```rust 69 | /// use firestore::FirestoreValue; 70 | /// 71 | /// let fv_string: FirestoreValue = "hello".into(); 72 | /// let fv_int: FirestoreValue = 42.into(); 73 | /// let fv_bool: FirestoreValue = true.into(); 74 | /// 75 | /// // Assuming MyStruct implements serde::Serialize 76 | /// // struct MyStruct { field: String } 77 | /// // let my_struct = MyStruct { field: "test".to_string() }; 78 | /// // let fv_struct: FirestoreValue = my_struct.into(); 79 | /// ``` 80 | impl std::convert::From for FirestoreValue 81 | where 82 | T: serde::Serialize, 83 | { 84 | fn from(value: T) -> Self { 85 | let serializer = crate::firestore_serde::serializer::FirestoreValueSerializer::new(); 86 | value.serialize(serializer).unwrap_or_else(|_err| { 87 | // It's generally better to panic or return a Result here if serialization 88 | // is critical and failure indicates a programming error. 89 | // However, matching existing behavior of defaulting to None/Null. 90 | // Consider logging the error: eprintln!("Failed to serialize to FirestoreValue: {}", err); 91 | FirestoreValue::from(Value { value_type: None }) 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/firestore_serde/null_serializers.rs: -------------------------------------------------------------------------------- 1 | pub(crate) const FIRESTORE_NULL_TYPE_TAG_TYPE: &str = "FirestoreNull"; 2 | 3 | pub mod serialize_as_null { 4 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 5 | 6 | pub fn serialize(date: &Option, serializer: S) -> Result 7 | where 8 | S: Serializer, 9 | T: Serialize, 10 | { 11 | serializer 12 | .serialize_newtype_struct(crate::firestore_serde::FIRESTORE_NULL_TYPE_TAG_TYPE, &date) 13 | } 14 | 15 | pub fn deserialize<'de, D, T>(deserializer: D) -> Result, D::Error> 16 | where 17 | D: Deserializer<'de>, 18 | T: for<'tde> Deserialize<'tde>, 19 | { 20 | Option::::deserialize(deserializer) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/firestore_serde/vector_serializers.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::FirestoreError; 2 | use crate::firestore_serde::serializer::FirestoreValueSerializer; 3 | use crate::FirestoreValue; 4 | use serde::de::{MapAccess, Visitor}; 5 | use serde::{Deserializer, Serialize}; 6 | 7 | pub(crate) const FIRESTORE_VECTOR_TYPE_TAG_TYPE: &str = "FirestoreVector"; 8 | 9 | #[derive(Serialize, Clone, Debug, PartialEq, PartialOrd, Default)] 10 | pub struct FirestoreVector(pub Vec); 11 | 12 | impl FirestoreVector { 13 | pub fn new(vec: Vec) -> Self { 14 | FirestoreVector(vec) 15 | } 16 | 17 | pub fn into_vec(self) -> Vec { 18 | self.0 19 | } 20 | 21 | pub fn as_vec(&self) -> &Vec { 22 | &self.0 23 | } 24 | } 25 | 26 | impl From for Vec { 27 | fn from(val: FirestoreVector) -> Self { 28 | val.into_vec() 29 | } 30 | } 31 | 32 | impl From for FirestoreVector 33 | where 34 | I: IntoIterator, 35 | { 36 | fn from(vec: I) -> Self { 37 | FirestoreVector(vec.into_iter().collect()) 38 | } 39 | } 40 | 41 | pub fn serialize_vector_for_firestore( 42 | firestore_value_serializer: FirestoreValueSerializer, 43 | value: &T, 44 | ) -> Result { 45 | let value_with_array = value.serialize(firestore_value_serializer)?; 46 | 47 | Ok(FirestoreValue::from( 48 | gcloud_sdk::google::firestore::v1::Value { 49 | value_type: Some(gcloud_sdk::google::firestore::v1::value::ValueType::MapValue( 50 | gcloud_sdk::google::firestore::v1::MapValue { 51 | fields: vec![ 52 | ( 53 | "__type__".to_string(), 54 | gcloud_sdk::google::firestore::v1::Value { 55 | value_type: Some(gcloud_sdk::google::firestore::v1::value::ValueType::StringValue( 56 | "__vector__".to_string() 57 | )), 58 | } 59 | ), 60 | ( 61 | "value".to_string(), 62 | value_with_array.value 63 | )].into_iter().collect() 64 | } 65 | )) 66 | }), 67 | ) 68 | } 69 | 70 | struct FirestoreVectorVisitor; 71 | 72 | impl<'de> Visitor<'de> for FirestoreVectorVisitor { 73 | type Value = FirestoreVector; 74 | 75 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 76 | formatter.write_str("a FirestoreVector") 77 | } 78 | 79 | fn visit_seq(self, mut seq: A) -> Result 80 | where 81 | A: serde::de::SeqAccess<'de>, 82 | { 83 | let mut vec = Vec::new(); 84 | 85 | while let Some(value) = seq.next_element()? { 86 | vec.push(value); 87 | } 88 | 89 | Ok(FirestoreVector(vec)) 90 | } 91 | 92 | fn visit_map(self, mut map: A) -> Result 93 | where 94 | A: MapAccess<'de>, 95 | { 96 | while let Some(field) = map.next_key::()? { 97 | match field.as_str() { 98 | "__type__" => { 99 | let value = map.next_value::()?; 100 | if value != "__vector__" { 101 | return Err(serde::de::Error::custom( 102 | "Expected __vector__ for FirestoreVector", 103 | )); 104 | } 105 | } 106 | "value" => { 107 | let value = map.next_value::>()?; 108 | return Ok(FirestoreVector(value)); 109 | } 110 | _ => { 111 | return Err(serde::de::Error::custom( 112 | "Unknown field for FirestoreVector", 113 | )); 114 | } 115 | } 116 | } 117 | Err(serde::de::Error::custom( 118 | "Unknown structure for FirestoreVector", 119 | )) 120 | } 121 | } 122 | 123 | impl<'de> serde::Deserialize<'de> for FirestoreVector { 124 | fn deserialize(deserializer: D) -> Result 125 | where 126 | D: Deserializer<'de>, 127 | { 128 | deserializer.deserialize_any(FirestoreVectorVisitor) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/firestore_value.rs: -------------------------------------------------------------------------------- 1 | use gcloud_sdk::google::firestore::v1::Value; 2 | use std::collections::HashMap; 3 | 4 | /// Represents a Firestore value, wrapping the underlying gRPC `Value` type. 5 | /// 6 | /// This struct provides a convenient way to work with Firestore's native data types 7 | /// and is used extensively throughout the crate, especially in serialization and 8 | /// deserialization, query filters, and field transformations. 9 | /// 10 | /// It can represent various types such as null, boolean, integer, double, timestamp, 11 | /// string, bytes, geo point, array, and map. 12 | /// 13 | /// Conversions from common Rust types to `FirestoreValue` are typically handled by 14 | /// the `From` trait implementations in the `firestore_serde` module (though not directly 15 | /// visible in this file, they are a core part of how `FirestoreValue` is used). 16 | /// 17 | /// # Examples 18 | /// 19 | /// ```rust 20 | /// use firestore::FirestoreValue; 21 | /// 22 | /// // Or, for direct construction of a map value: 23 | /// let fv_map = FirestoreValue::from_map(vec![ 24 | /// ("name", "Alice".into()), // .into() relies on From for FirestoreValue 25 | /// ("age", 30.into()), 26 | /// ]); 27 | /// ``` 28 | #[derive(Debug, PartialEq, Clone)] 29 | pub struct FirestoreValue { 30 | /// The underlying gRPC `Value` protobuf message. 31 | pub value: Value, 32 | } 33 | 34 | impl FirestoreValue { 35 | /// Creates a `FirestoreValue` directly from a `gcloud_sdk::google::firestore::v1::Value`. 36 | pub fn from(value: Value) -> Self { 37 | Self { value } 38 | } 39 | 40 | /// Creates a `FirestoreValue` representing a Firestore map from an iterator of key-value pairs. 41 | /// 42 | /// # Type Parameters 43 | /// * `I`: An iterator type yielding pairs of field names and their `FirestoreValue`s. 44 | /// * `IS`: A type that can be converted into a string for field names. 45 | /// 46 | /// # Arguments 47 | /// * `fields`: An iterator providing the map's fields. 48 | pub fn from_map(fields: I) -> Self 49 | where 50 | I: IntoIterator, 51 | IS: AsRef, 52 | { 53 | let fields: HashMap = fields 54 | .into_iter() 55 | .map(|(k, v)| (k.as_ref().to_string(), v.value)) 56 | .collect(); 57 | Self::from(Value { 58 | value_type: Some( 59 | gcloud_sdk::google::firestore::v1::value::ValueType::MapValue( 60 | gcloud_sdk::google::firestore::v1::MapValue { fields }, 61 | ), 62 | ), 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/fluent_api/mod.rs: -------------------------------------------------------------------------------- 1 | //! Provides a fluent, chainable API for constructing and executing Firestore operations. 2 | //! 3 | //! This module is the entry point for the fluent API, which allows for a more declarative 4 | //! and type-safe way to interact with Firestore compared to using the direct methods on 5 | //! [`FirestoreDb`](crate::FirestoreDb) with [`FirestoreQueryParams`](crate::FirestoreQueryParams). 6 | //! 7 | //! The main way to access this API is via the [`FirestoreDb::fluent()`](crate::FirestoreDb::fluent) method, 8 | //! which returns a [`FirestoreExprBuilder`]. From there, you can chain calls to build 9 | //! `select`, `insert`, `update`, `delete`, or `list` operations. 10 | //! 11 | //! Each operation type has its own dedicated builder module: 12 | //! - [`delete_builder`]: For constructing delete operations. 13 | //! - [`document_transform_builder`]: For specifying field transformations in update operations. 14 | //! - [`insert_builder`]: For constructing insert/create operations. 15 | //! - [`listing_builder`]: For listing documents or collection IDs. 16 | //! - [`select_aggregation_builder`]: For building aggregation queries (e.g., count, sum, avg). 17 | //! - [`select_builder`]: For constructing query/select operations. 18 | //! - [`select_filter_builder`]: For building complex filter conditions for queries. 19 | //! - [`update_builder`]: For constructing update operations. 20 | //! ``` 21 | 22 | // Linter allowance for functions that might have many arguments, 23 | // often seen in builder patterns or comprehensive configuration methods. 24 | #![allow(clippy::too_many_arguments)] 25 | 26 | pub mod delete_builder; 27 | pub mod document_transform_builder; 28 | pub mod insert_builder; 29 | pub mod listing_builder; 30 | pub mod select_aggregation_builder; 31 | pub mod select_builder; 32 | pub mod select_filter_builder; 33 | pub mod update_builder; 34 | 35 | use crate::delete_builder::FirestoreDeleteInitialBuilder; 36 | use crate::fluent_api::select_builder::FirestoreSelectInitialBuilder; 37 | use crate::insert_builder::FirestoreInsertInitialBuilder; 38 | use crate::listing_builder::FirestoreListingInitialBuilder; 39 | use crate::update_builder::FirestoreUpdateInitialBuilder; 40 | use crate::{ 41 | FirestoreAggregatedQuerySupport, FirestoreCreateSupport, FirestoreDb, FirestoreDeleteSupport, 42 | FirestoreGetByIdSupport, FirestoreListenSupport, FirestoreListingSupport, 43 | FirestoreQuerySupport, FirestoreUpdateSupport, 44 | }; 45 | 46 | /// The entry point for building fluent Firestore expressions. 47 | /// 48 | /// Obtain an instance of this builder by calling [`FirestoreDb::fluent()`](crate::FirestoreDb::fluent). 49 | /// From this builder, you can chain methods to specify the type of operation 50 | /// (select, insert, update, delete, list) and then further configure and execute it. 51 | /// 52 | /// The type parameter `D` represents the underlying database client type, which 53 | /// must implement various support traits (like [`FirestoreQuerySupport`], [`FirestoreCreateSupport`], etc.). 54 | /// This is typically [`FirestoreDb`](crate::FirestoreDb). 55 | #[derive(Clone, Debug)] 56 | pub struct FirestoreExprBuilder<'a, D> { 57 | db: &'a D, 58 | } 59 | 60 | impl<'a, D> FirestoreExprBuilder<'a, D> 61 | where 62 | D: FirestoreQuerySupport 63 | + FirestoreCreateSupport 64 | + FirestoreDeleteSupport 65 | + FirestoreUpdateSupport 66 | + FirestoreListingSupport 67 | + FirestoreGetByIdSupport 68 | + FirestoreListenSupport 69 | + FirestoreAggregatedQuerySupport 70 | + Clone 71 | + Send 72 | + Sync 73 | + 'static, 74 | { 75 | /// Creates a new `FirestoreExprBuilder` with a reference to the database client. 76 | /// This is typically called by [`FirestoreDb::fluent()`](crate::FirestoreDb::fluent). 77 | pub(crate) fn new(db: &'a D) -> Self { 78 | Self { db } 79 | } 80 | 81 | /// Begins building a Firestore select/query operation. 82 | /// 83 | /// Returns a [`FirestoreSelectInitialBuilder`] to further configure the query. 84 | #[inline] 85 | pub fn select(self) -> FirestoreSelectInitialBuilder<'a, D> { 86 | FirestoreSelectInitialBuilder::new(self.db) 87 | } 88 | 89 | /// Begins building a Firestore insert/create operation. 90 | /// 91 | /// Returns a [`FirestoreInsertInitialBuilder`] to further configure the insertion. 92 | #[inline] 93 | pub fn insert(self) -> FirestoreInsertInitialBuilder<'a, D> { 94 | FirestoreInsertInitialBuilder::new(self.db) 95 | } 96 | 97 | /// Begins building a Firestore update operation. 98 | /// 99 | /// Returns a [`FirestoreUpdateInitialBuilder`] to further configure the update. 100 | #[inline] 101 | pub fn update(self) -> FirestoreUpdateInitialBuilder<'a, D> { 102 | FirestoreUpdateInitialBuilder::new(self.db) 103 | } 104 | 105 | /// Begins building a Firestore delete operation. 106 | /// 107 | /// Returns a [`FirestoreDeleteInitialBuilder`] to further configure the deletion. 108 | #[inline] 109 | pub fn delete(self) -> FirestoreDeleteInitialBuilder<'a, D> { 110 | FirestoreDeleteInitialBuilder::new(self.db) 111 | } 112 | 113 | /// Begins building a Firestore list operation (e.g., listing documents in a collection 114 | /// or listing collection IDs). 115 | /// 116 | /// Returns a [`FirestoreListingInitialBuilder`] to further configure the listing operation. 117 | #[inline] 118 | pub fn list(self) -> FirestoreListingInitialBuilder<'a, D> { 119 | FirestoreListingInitialBuilder::new(self.db) 120 | } 121 | } 122 | 123 | impl FirestoreDb { 124 | /// Provides access to the fluent API for building Firestore operations. 125 | /// 126 | /// This is the main entry point for using the chainable builder pattern. 127 | #[inline] 128 | pub fn fluent(&self) -> FirestoreExprBuilder { 129 | FirestoreExprBuilder::new(self) 130 | } 131 | } 132 | 133 | #[cfg(test)] 134 | pub(crate) mod tests { 135 | pub mod mockdb; 136 | 137 | // Test structure used in fluent API examples and tests. 138 | pub struct TestStructure { 139 | pub some_id: String, 140 | pub one_more_string: String, 141 | pub some_num: u64, 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/struct_path_macro.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! path { 3 | ($($x:tt)*) => {{ 4 | $crate::struct_path::path!($($x)*).to_string() 5 | }}; 6 | } 7 | 8 | #[macro_export] 9 | macro_rules! paths { 10 | ($($x:tt)*) => {{ 11 | $crate::struct_path::paths!($($x)*).iter().map(|s| s.to_string()).collect::>() 12 | }}; 13 | } 14 | 15 | #[macro_export] 16 | macro_rules! path_camel_case { 17 | ($($x:tt)*) => {{ 18 | $crate::struct_path::path!($($x)*;case="camel").to_string() 19 | }}; 20 | } 21 | 22 | #[macro_export] 23 | macro_rules! paths_camel_case { 24 | ($($x:tt)*) => {{ 25 | $crate::struct_path::paths!($($x)*;case="camel").into_iter().map(|s| s.to_string()).collect::>() 26 | }} 27 | } 28 | -------------------------------------------------------------------------------- /src/timestamp_utils.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::*; 2 | use crate::FirestoreResult; 3 | use chrono::prelude::*; 4 | 5 | /// Converts a Google `prost_types::Timestamp` to a `chrono::DateTime`. 6 | /// 7 | /// Firestore uses Google's `Timestamp` protobuf message to represent timestamps. 8 | /// This function facilitates conversion to the more commonly used `chrono::DateTime` 9 | /// in Rust applications. 10 | /// 11 | /// # Arguments 12 | /// * `ts`: The Google `Timestamp` to convert. 13 | /// 14 | /// # Returns 15 | /// A `FirestoreResult` containing the `DateTime` on success, or a 16 | /// `FirestoreError::DeserializeError` if the timestamp is invalid or out of range. 17 | /// 18 | /// # Examples 19 | /// ```rust 20 | /// use firestore::timestamp_utils::from_timestamp; 21 | /// use chrono::{Utc, TimeZone}; 22 | /// 23 | /// let prost_timestamp = gcloud_sdk::prost_types::Timestamp { seconds: 1670000000, nanos: 0 }; 24 | /// let chrono_datetime = from_timestamp(prost_timestamp).unwrap(); 25 | /// 26 | /// assert_eq!(chrono_datetime, Utc.with_ymd_and_hms(2022, 12, 2, 16, 53, 20).unwrap()); 27 | /// ``` 28 | pub fn from_timestamp(ts: gcloud_sdk::prost_types::Timestamp) -> FirestoreResult> { 29 | if let Some(dt) = chrono::DateTime::from_timestamp(ts.seconds, ts.nanos as u32) { 30 | Ok(dt) 31 | } else { 32 | Err(FirestoreError::DeserializeError( 33 | FirestoreSerializationError::from_message(format!( 34 | "Invalid or out-of-range datetime: {ts:?}" // Added :? for better debug output 35 | )), 36 | )) 37 | } 38 | } 39 | 40 | /// Converts a `chrono::DateTime` to a Google `prost_types::Timestamp`. 41 | /// 42 | /// This is the reverse of [`from_timestamp`], used when sending timestamp data 43 | /// to Firestore. 44 | /// 45 | /// # Arguments 46 | /// * `dt`: The `chrono::DateTime` to convert. 47 | /// 48 | /// # Returns 49 | /// The corresponding Google `Timestamp`. 50 | /// 51 | /// # Examples 52 | /// ```rust 53 | /// use firestore::timestamp_utils::to_timestamp; 54 | /// use chrono::{Utc, TimeZone}; 55 | /// 56 | /// let chrono_datetime = Utc.with_ymd_and_hms(2022, 12, 2, 16, 53, 20).unwrap(); 57 | /// let prost_timestamp = to_timestamp(chrono_datetime); 58 | /// 59 | /// assert_eq!(prost_timestamp.seconds, 1670000000); 60 | /// assert_eq!(prost_timestamp.nanos, 0); 61 | /// ``` 62 | pub fn to_timestamp(dt: DateTime) -> gcloud_sdk::prost_types::Timestamp { 63 | gcloud_sdk::prost_types::Timestamp { 64 | seconds: dt.timestamp(), 65 | nanos: dt.nanosecond() as i32, 66 | } 67 | } 68 | 69 | /// Converts a Google `prost_types::Duration` to a `chrono::Duration`. 70 | /// 71 | /// Google's `Duration` protobuf message is used in some Firestore contexts, 72 | /// for example, in query execution statistics. 73 | /// 74 | /// # Arguments 75 | /// * `duration`: The Google `Duration` to convert. 76 | /// 77 | /// # Returns 78 | /// The corresponding `chrono::Duration`. 79 | /// 80 | /// # Examples 81 | /// ```rust 82 | /// use firestore::timestamp_utils::from_duration; 83 | /// 84 | /// let prost_duration = gcloud_sdk::prost_types::Duration { seconds: 5, nanos: 500_000_000 }; 85 | /// let chrono_duration = from_duration(prost_duration); 86 | /// 87 | /// assert_eq!(chrono_duration, chrono::Duration::milliseconds(5500)); 88 | /// ``` 89 | pub fn from_duration(duration: gcloud_sdk::prost_types::Duration) -> chrono::Duration { 90 | chrono::Duration::seconds(duration.seconds) 91 | + chrono::Duration::nanoseconds(duration.nanos.into()) 92 | } 93 | -------------------------------------------------------------------------------- /tests/caching_memory_test.rs: -------------------------------------------------------------------------------- 1 | use crate::common::{eventually_async, populate_collection, setup}; 2 | use futures::TryStreamExt; 3 | use serde::{Deserialize, Serialize}; 4 | use std::time::Duration; 5 | 6 | mod common; 7 | use firestore::*; 8 | 9 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] 10 | struct MyTestStructure { 11 | some_id: String, 12 | some_string: String, 13 | } 14 | 15 | #[tokio::test] 16 | async fn precondition_tests() -> Result<(), Box> { 17 | let db = setup().await?; 18 | 19 | const TEST_COLLECTION_NAME_NO_PRELOAD: &'static str = "integration-test-caching-mem-no-preload"; 20 | const TEST_COLLECTION_NAME_PRELOAD: &'static str = "integration-test-caching-mem-preload"; 21 | 22 | populate_collection( 23 | &db, 24 | TEST_COLLECTION_NAME_NO_PRELOAD, 25 | 10, 26 | |i| MyTestStructure { 27 | some_id: format!("test-{}", i), 28 | some_string: format!("Test value {}", i), 29 | }, 30 | |ms| ms.some_id.clone(), 31 | ) 32 | .await?; 33 | 34 | populate_collection( 35 | &db, 36 | TEST_COLLECTION_NAME_PRELOAD, 37 | 10, 38 | |i| MyTestStructure { 39 | some_id: format!("test-{}", i), 40 | some_string: format!("Test value {}", i), 41 | }, 42 | |ms| ms.some_id.clone(), 43 | ) 44 | .await?; 45 | 46 | let mut cache = FirestoreCache::new( 47 | "example-mem-cache".into(), 48 | &db, 49 | FirestoreMemoryCacheBackend::new( 50 | FirestoreCacheConfiguration::new() 51 | .add_collection_config( 52 | &db, 53 | FirestoreCacheCollectionConfiguration::new( 54 | TEST_COLLECTION_NAME_NO_PRELOAD, 55 | FirestoreListenerTarget::new(1000), 56 | FirestoreCacheCollectionLoadMode::PreloadNone, 57 | ), 58 | ) 59 | .add_collection_config( 60 | &db, 61 | FirestoreCacheCollectionConfiguration::new( 62 | TEST_COLLECTION_NAME_PRELOAD, 63 | FirestoreListenerTarget::new(1001), 64 | FirestoreCacheCollectionLoadMode::PreloadAllDocs, 65 | ), 66 | ), 67 | )?, 68 | FirestoreMemListenStateStorage::new(), 69 | ) 70 | .await?; 71 | 72 | cache.load().await?; 73 | 74 | let my_struct: Option = db 75 | .read_cached_only(&cache) 76 | .fluent() 77 | .select() 78 | .by_id_in(TEST_COLLECTION_NAME_NO_PRELOAD) 79 | .obj() 80 | .one("test-0") 81 | .await?; 82 | 83 | assert!(my_struct.is_none()); 84 | 85 | let my_struct: Option = db 86 | .read_cached_only(&cache) 87 | .fluent() 88 | .select() 89 | .by_id_in(TEST_COLLECTION_NAME_PRELOAD) 90 | .obj() 91 | .one("test-0") 92 | .await?; 93 | 94 | assert!(my_struct.is_some()); 95 | 96 | db.read_through_cache(&cache) 97 | .fluent() 98 | .select() 99 | .by_id_in(TEST_COLLECTION_NAME_NO_PRELOAD) 100 | .obj::() 101 | .one("test-1") 102 | .await?; 103 | 104 | let my_struct: Option = db 105 | .read_cached_only(&cache) 106 | .fluent() 107 | .select() 108 | .by_id_in(TEST_COLLECTION_NAME_NO_PRELOAD) 109 | .obj() 110 | .one("test-1") 111 | .await?; 112 | 113 | assert!(my_struct.is_some()); 114 | 115 | let cached_db = db.read_cached_only(&cache); 116 | let all_items_stream = cached_db 117 | .fluent() 118 | .list() 119 | .from(TEST_COLLECTION_NAME_NO_PRELOAD) 120 | .obj::() 121 | .stream_all_with_errors() 122 | .await?; 123 | 124 | let all_items = all_items_stream.try_collect::>().await?; 125 | 126 | assert_eq!(all_items.len(), 1); 127 | 128 | let all_items_stream = cached_db 129 | .fluent() 130 | .list() 131 | .from(TEST_COLLECTION_NAME_PRELOAD) 132 | .obj::() 133 | .stream_all_with_errors() 134 | .await?; 135 | 136 | let all_items = all_items_stream.try_collect::>().await?; 137 | 138 | assert_eq!(all_items.len(), 10); 139 | 140 | db.fluent() 141 | .update() 142 | .fields(paths!(MyTestStructure::some_string)) 143 | .in_col(TEST_COLLECTION_NAME_PRELOAD) 144 | .document_id("test-2") 145 | .object(&MyTestStructure { 146 | some_id: "test-2".to_string(), 147 | some_string: "updated".to_string(), 148 | }) 149 | .execute::<()>() 150 | .await?; 151 | 152 | let cached_db = db.read_cached_only(&cache); 153 | assert!( 154 | eventually_async(10, Duration::from_millis(500), move || { 155 | let cached_db = cached_db.clone(); 156 | async move { 157 | let my_struct: Option = cached_db 158 | .fluent() 159 | .select() 160 | .by_id_in(TEST_COLLECTION_NAME_PRELOAD) 161 | .obj() 162 | .one("test-2") 163 | .await?; 164 | 165 | if let Some(my_struct) = my_struct { 166 | return Ok(my_struct.some_string.as_str() == "updated"); 167 | } 168 | Ok(false) 169 | } 170 | }) 171 | .await? 172 | ); 173 | 174 | cache.shutdown().await?; 175 | 176 | Ok(()) 177 | } 178 | -------------------------------------------------------------------------------- /tests/caching_persistent_test.rs: -------------------------------------------------------------------------------- 1 | use crate::common::{eventually_async, populate_collection, setup}; 2 | use futures::TryStreamExt; 3 | use serde::{Deserialize, Serialize}; 4 | use std::time::Duration; 5 | use tokio::time::sleep; 6 | 7 | mod common; 8 | use firestore::*; 9 | 10 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] 11 | struct MyTestStructure { 12 | some_id: String, 13 | some_num: u64, 14 | some_string: String, 15 | } 16 | 17 | #[tokio::test] 18 | async fn precondition_tests() -> Result<(), Box> { 19 | let db = setup().await?; 20 | 21 | const TEST_COLLECTION_NAME_NO_PRELOAD: &'static str = 22 | "integration-test-caching-persistent-no-preload"; 23 | const TEST_COLLECTION_NAME_PRELOAD: &'static str = 24 | "integration-test-caching-persistent-preload"; 25 | 26 | populate_collection( 27 | &db, 28 | TEST_COLLECTION_NAME_NO_PRELOAD, 29 | 10, 30 | |i| MyTestStructure { 31 | some_id: format!("test-{}", i), 32 | some_num: i as u64, 33 | some_string: format!("Test value {}", i), 34 | }, 35 | |ms| ms.some_id.clone(), 36 | ) 37 | .await?; 38 | 39 | populate_collection( 40 | &db, 41 | TEST_COLLECTION_NAME_PRELOAD, 42 | 10, 43 | |i| MyTestStructure { 44 | some_id: format!("test-{}", i), 45 | some_num: i as u64, 46 | some_string: format!("Test value {}", i), 47 | }, 48 | |ms| ms.some_id.clone(), 49 | ) 50 | .await?; 51 | sleep(Duration::from_secs(1)).await; 52 | 53 | let temp_state_dir = tempfile::tempdir()?; 54 | let temp_db_dir = tempfile::tempdir()?; 55 | 56 | let mut cache = FirestoreCache::new( 57 | "example-persistent-cache".into(), 58 | &db, 59 | FirestorePersistentCacheBackend::with_options( 60 | FirestoreCacheConfiguration::new() 61 | .add_collection_config( 62 | &db, 63 | FirestoreCacheCollectionConfiguration::new( 64 | TEST_COLLECTION_NAME_NO_PRELOAD, 65 | FirestoreListenerTarget::new(1000), 66 | FirestoreCacheCollectionLoadMode::PreloadNone, 67 | ), 68 | ) 69 | .add_collection_config( 70 | &db, 71 | FirestoreCacheCollectionConfiguration::new( 72 | TEST_COLLECTION_NAME_PRELOAD, 73 | FirestoreListenerTarget::new(1001), 74 | FirestoreCacheCollectionLoadMode::PreloadAllDocs, 75 | ), 76 | ), 77 | temp_db_dir.into_path().join("redb"), 78 | )?, 79 | FirestoreTempFilesListenStateStorage::with_temp_dir(temp_state_dir.into_path()), 80 | ) 81 | .await?; 82 | 83 | cache.load().await?; 84 | 85 | let my_struct: Option = db 86 | .read_cached_only(&cache) 87 | .fluent() 88 | .select() 89 | .by_id_in(TEST_COLLECTION_NAME_NO_PRELOAD) 90 | .obj() 91 | .one("test-0") 92 | .await?; 93 | 94 | assert!(my_struct.is_none()); 95 | 96 | let my_struct: Option = db 97 | .read_cached_only(&cache) 98 | .fluent() 99 | .select() 100 | .by_id_in(TEST_COLLECTION_NAME_PRELOAD) 101 | .obj() 102 | .one("test-0") 103 | .await?; 104 | 105 | assert!(my_struct.is_some()); 106 | 107 | db.read_through_cache(&cache) 108 | .fluent() 109 | .select() 110 | .by_id_in(TEST_COLLECTION_NAME_NO_PRELOAD) 111 | .obj::() 112 | .one("test-1") 113 | .await?; 114 | 115 | let my_struct: Option = db 116 | .read_cached_only(&cache) 117 | .fluent() 118 | .select() 119 | .by_id_in(TEST_COLLECTION_NAME_NO_PRELOAD) 120 | .obj() 121 | .one("test-1") 122 | .await?; 123 | 124 | assert!(my_struct.is_some()); 125 | 126 | let cached_db = db.read_cached_only(&cache); 127 | let all_items_stream = cached_db 128 | .fluent() 129 | .list() 130 | .from(TEST_COLLECTION_NAME_NO_PRELOAD) 131 | .obj::() 132 | .stream_all_with_errors() 133 | .await?; 134 | 135 | let all_items = all_items_stream.try_collect::>().await?; 136 | 137 | assert_eq!(all_items.len(), 1); 138 | 139 | let all_items_stream = cached_db 140 | .fluent() 141 | .list() 142 | .from(TEST_COLLECTION_NAME_PRELOAD) 143 | .obj::() 144 | .stream_all_with_errors() 145 | .await?; 146 | 147 | let all_items = all_items_stream.try_collect::>().await?; 148 | 149 | assert_eq!(all_items.len(), 10); 150 | 151 | db.fluent() 152 | .update() 153 | .fields(paths!(MyTestStructure::some_string)) 154 | .in_col(TEST_COLLECTION_NAME_PRELOAD) 155 | .document_id("test-2") 156 | .object(&MyTestStructure { 157 | some_id: "test-2".to_string(), 158 | some_num: 2, 159 | some_string: "updated".to_string(), 160 | }) 161 | .execute::<()>() 162 | .await?; 163 | 164 | let cached_db = db.read_cached_only(&cache); 165 | assert!( 166 | eventually_async(10, Duration::from_millis(500), move || { 167 | let cached_db = cached_db.clone(); 168 | async move { 169 | let my_struct: Option = cached_db 170 | .fluent() 171 | .select() 172 | .by_id_in(TEST_COLLECTION_NAME_PRELOAD) 173 | .obj() 174 | .one("test-2") 175 | .await?; 176 | 177 | if let Some(my_struct) = my_struct { 178 | return Ok(my_struct.some_string.as_str() == "updated"); 179 | } 180 | Ok(false) 181 | } 182 | }) 183 | .await? 184 | ); 185 | 186 | let cached_db = db.read_cached_only(&cache); 187 | let queried = cached_db 188 | .fluent() 189 | .select() 190 | .from(TEST_COLLECTION_NAME_PRELOAD) 191 | .filter(|q| { 192 | q.for_all([q 193 | .field(path!(MyTestStructure::some_num)) 194 | .greater_than_or_equal(5)]) 195 | }) 196 | .order_by([( 197 | path!(MyTestStructure::some_num), 198 | FirestoreQueryDirection::Descending, 199 | )]) 200 | .obj::() 201 | .stream_query_with_errors() 202 | .await?; 203 | 204 | let queried_items = queried.try_collect::>().await?; 205 | assert_eq!(queried_items.len(), 5); 206 | assert_eq!(queried_items.first().map(|d| d.some_num), Some(9)); 207 | 208 | cache.shutdown().await?; 209 | 210 | Ok(()) 211 | } 212 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use firestore::*; 2 | use futures::future::BoxFuture; 3 | use futures::FutureExt; 4 | use serde::{Deserialize, Serialize}; 5 | use std::future::Future; 6 | use std::ops::Mul; 7 | use tokio::time::sleep; 8 | use tracing::*; 9 | 10 | #[allow(dead_code)] 11 | pub fn config_env_var(name: &str) -> Result { 12 | std::env::var(name).map_err(|e| format!("{}: {}", name, e)) 13 | } 14 | 15 | #[allow(dead_code)] 16 | pub async fn setup() -> Result> { 17 | // Logging with debug enabled 18 | let filter = 19 | tracing_subscriber::EnvFilter::builder().parse("info,firestore=debug,gcloud_sdk=debug")?; 20 | 21 | let subscriber = tracing_subscriber::fmt().with_env_filter(filter).finish(); 22 | tracing::subscriber::set_global_default(subscriber)?; 23 | 24 | // Create an instance 25 | let db = FirestoreDb::new(&config_env_var("GCP_PROJECT")?).await?; 26 | 27 | Ok(db) 28 | } 29 | 30 | #[allow(dead_code)] 31 | pub async fn populate_collection( 32 | db: &FirestoreDb, 33 | collection_name: &str, 34 | max_items: usize, 35 | sf: fn(usize) -> T, 36 | df: DF, 37 | ) -> Result<(), Box> 38 | where 39 | T: Serialize + Send + Sync + 'static, 40 | for<'de> T: Deserialize<'de>, 41 | DF: Fn(&T) -> String, 42 | { 43 | info!(collection_name, "Populating collection."); 44 | let batch_writer = db.create_simple_batch_writer().await?; 45 | let mut current_batch = batch_writer.new_batch(); 46 | 47 | for i in 0..max_items { 48 | let my_struct = sf(i); 49 | 50 | // Let's insert some data 51 | db.fluent() 52 | .update() 53 | .in_col(collection_name) 54 | .document_id(df(&my_struct).as_str()) 55 | .object(&my_struct) 56 | .add_to_batch(&mut current_batch)?; 57 | } 58 | current_batch.write().await?; 59 | Ok(()) 60 | } 61 | 62 | #[allow(dead_code)] 63 | pub fn eventually_async<'a, F, FN>( 64 | max_retries: u32, 65 | sleep_duration: std::time::Duration, 66 | f: FN, 67 | ) -> BoxFuture<'a, Result>> 68 | where 69 | FN: Fn() -> F + Send + Sync + 'a, 70 | F: Future>> + Send + 'a, 71 | { 72 | async move { 73 | let mut retries = 0; 74 | loop { 75 | if f().await? { 76 | return Ok(true); 77 | } 78 | retries += 1; 79 | if retries > max_retries { 80 | return Ok(false); 81 | } 82 | sleep(sleep_duration.mul(retries * retries)).await; 83 | } 84 | } 85 | .boxed() 86 | } 87 | 88 | #[derive(Debug)] 89 | pub struct CustomUserError { 90 | details: String, 91 | } 92 | 93 | #[allow(dead_code)] 94 | impl CustomUserError { 95 | pub fn new(msg: &str) -> CustomUserError { 96 | CustomUserError { 97 | details: msg.to_string(), 98 | } 99 | } 100 | } 101 | 102 | impl std::fmt::Display for CustomUserError { 103 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 104 | write!(f, "{}", self.details) 105 | } 106 | } 107 | 108 | impl std::error::Error for CustomUserError { 109 | fn description(&self) -> &str { 110 | &self.details 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/complex-structure-serialize.rs: -------------------------------------------------------------------------------- 1 | use approx::relative_eq; 2 | use chrono::{DateTime, Utc}; 3 | use firestore::*; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | mod common; 7 | 8 | use crate::common::setup; 9 | 10 | #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] 11 | pub struct Test1(pub u8); 12 | 13 | #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] 14 | pub struct Test1i(pub Test1); 15 | 16 | #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] 17 | pub struct Test2 { 18 | some_id: String, 19 | some_bool: Option, 20 | } 21 | 22 | #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] 23 | pub enum TestEnum { 24 | TestChoice, 25 | TestWithParam(String), 26 | TestWithMultipleParams(String, String), 27 | TestWithStruct(Test2), 28 | } 29 | 30 | // Example structure to play with 31 | #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] 32 | struct MyTestStructure { 33 | some_id: String, 34 | some_string: String, 35 | some_num: u64, 36 | #[serde(with = "firestore::serialize_as_timestamp")] 37 | created_at: DateTime, 38 | #[serde(default)] 39 | #[serde(with = "firestore::serialize_as_optional_timestamp")] 40 | updated_at: Option>, 41 | #[serde(default)] 42 | #[serde(with = "firestore::serialize_as_null_timestamp")] 43 | updated_at_as_null: Option>, 44 | test1: Test1, 45 | test1i: Test1i, 46 | test11: Option, 47 | test2: Option, 48 | test3: Vec, 49 | test4: TestEnum, 50 | test5: (TestEnum, TestEnum), 51 | test6: TestEnum, 52 | test7: TestEnum, 53 | #[serde(default)] 54 | #[serde(with = "firestore::serialize_as_null")] 55 | test_null1: Option, 56 | #[serde(default)] 57 | #[serde(with = "firestore::serialize_as_null")] 58 | test_null2: Option, 59 | } 60 | 61 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] 62 | struct MyFloatStructure { 63 | some_f32: f32, 64 | some_f64: f64, 65 | } 66 | 67 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] 68 | struct MyVectorStructure { 69 | some_vec: FirestoreVector, 70 | } 71 | 72 | #[tokio::test] 73 | async fn main() -> Result<(), Box> { 74 | let db = setup().await?; 75 | 76 | const TEST_COLLECTION_NAME: &str = "integration-test-complex"; 77 | 78 | let my_struct = MyTestStructure { 79 | some_id: "test-1".to_string(), 80 | some_string: "Test".to_string(), 81 | some_num: 41, 82 | created_at: Utc::now(), 83 | updated_at: None, 84 | updated_at_as_null: None, 85 | test1: Test1(1), 86 | test1i: Test1i(Test1(1)), 87 | test11: Some(Test1(1)), 88 | test2: Some(Test2 { 89 | some_id: "test-1".to_string(), 90 | some_bool: Some(true), 91 | }), 92 | test3: vec![ 93 | Test2 { 94 | some_id: "test-2".to_string(), 95 | some_bool: Some(false), 96 | }, 97 | Test2 { 98 | some_id: "test-2".to_string(), 99 | some_bool: Some(true), 100 | }, 101 | ], 102 | test4: TestEnum::TestChoice, 103 | test5: (TestEnum::TestChoice, TestEnum::TestChoice), 104 | test6: TestEnum::TestWithMultipleParams("ss".to_string(), "ss".to_string()), 105 | test7: TestEnum::TestWithStruct(Test2 { 106 | some_id: "test-2".to_string(), 107 | some_bool: Some(true), 108 | }), 109 | test_null1: None, 110 | test_null2: Some("Test".to_string()), 111 | }; 112 | 113 | // Remove if it already exist 114 | db.delete_by_id(TEST_COLLECTION_NAME, &my_struct.some_id, None) 115 | .await?; 116 | 117 | // Let's insert some data 118 | db.create_obj::<_, (), _>( 119 | TEST_COLLECTION_NAME, 120 | Some(&my_struct.some_id), 121 | &my_struct, 122 | None, 123 | ) 124 | .await?; 125 | 126 | let to_update = MyTestStructure { 127 | some_num: my_struct.some_num + 1, 128 | some_string: "updated-value".to_string(), 129 | ..my_struct.clone() 130 | }; 131 | 132 | // Update some field in it 133 | let updated_obj: MyTestStructure = db 134 | .update_obj( 135 | TEST_COLLECTION_NAME, 136 | &my_struct.some_id, 137 | &to_update, 138 | Some(paths!(MyTestStructure::{ 139 | some_num, 140 | some_string 141 | })), 142 | None, 143 | None, 144 | ) 145 | .await?; 146 | 147 | // Get object by id 148 | let find_it_again: MyTestStructure = 149 | db.get_obj(TEST_COLLECTION_NAME, &my_struct.some_id).await?; 150 | 151 | assert_eq!(updated_obj.some_num, to_update.some_num); 152 | println!("updated_obj.some_num: {:?}", to_update.some_num); 153 | 154 | assert_eq!(updated_obj.some_string, to_update.some_string); 155 | assert_eq!(updated_obj.test1, to_update.test1); 156 | 157 | assert_eq!(updated_obj.some_num, find_it_again.some_num); 158 | assert_eq!(updated_obj.some_string, find_it_again.some_string); 159 | assert_eq!(updated_obj.test1, find_it_again.test1); 160 | 161 | let my_float_structure = MyFloatStructure { 162 | some_f32: 42.0, 163 | some_f64: 42.0, 164 | }; 165 | let my_float_structure_returned: MyFloatStructure = db 166 | .fluent() 167 | .update() 168 | .in_col(TEST_COLLECTION_NAME) 169 | .document_id("test-floats") 170 | .object(&my_float_structure) 171 | .execute() 172 | .await?; 173 | 174 | assert!(relative_eq!( 175 | my_float_structure_returned.some_f32, 176 | my_float_structure.some_f32 177 | )); 178 | assert!(relative_eq!( 179 | my_float_structure_returned.some_f64, 180 | my_float_structure.some_f64 181 | )); 182 | 183 | let my_vector_structure = MyVectorStructure { 184 | some_vec: FirestoreVector::new(vec![1.0, 2.0, 3.0]), 185 | }; 186 | let my_vector_structure_returned: MyVectorStructure = db 187 | .fluent() 188 | .update() 189 | .in_col(TEST_COLLECTION_NAME) 190 | .document_id("test-vectors") 191 | .object(&my_vector_structure) 192 | .execute() 193 | .await?; 194 | 195 | assert_eq!( 196 | my_vector_structure.some_vec, 197 | my_vector_structure_returned.some_vec 198 | ); 199 | 200 | Ok(()) 201 | } 202 | -------------------------------------------------------------------------------- /tests/create-option-tests.rs: -------------------------------------------------------------------------------- 1 | use crate::common::setup; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | mod common; 5 | use firestore::*; 6 | 7 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] 8 | struct MyTestStructure { 9 | some_id: String, 10 | some_string: Option, 11 | } 12 | 13 | #[tokio::test] 14 | async fn crud_tests() -> Result<(), Box> { 15 | const TEST_COLLECTION_NAME: &str = "integration-test-options"; 16 | 17 | let db = setup().await?; 18 | 19 | let my_struct1 = MyTestStructure { 20 | some_id: "test-0".to_string(), 21 | some_string: Some("some_string".to_string()), 22 | }; 23 | 24 | db.fluent() 25 | .delete() 26 | .from(TEST_COLLECTION_NAME) 27 | .document_id(&my_struct1.some_id) 28 | .execute() 29 | .await?; 30 | 31 | let object_returned: MyTestStructure = db 32 | .fluent() 33 | .insert() 34 | .into(TEST_COLLECTION_NAME) 35 | .document_id(&my_struct1.some_id) 36 | .object(&my_struct1) 37 | .execute() 38 | .await?; 39 | 40 | assert_eq!(object_returned, my_struct1); 41 | 42 | let object_updated: MyTestStructure = db 43 | .fluent() 44 | .update() 45 | .fields(paths!(MyTestStructure::{some_string})) 46 | .in_col(TEST_COLLECTION_NAME) 47 | .document_id(&my_struct1.some_id) 48 | .object(&MyTestStructure { 49 | some_string: None, 50 | ..my_struct1.clone() 51 | }) 52 | .execute() 53 | .await?; 54 | 55 | assert_eq!( 56 | object_updated, 57 | MyTestStructure { 58 | some_string: None, 59 | ..my_struct1.clone() 60 | } 61 | ); 62 | 63 | let find_it_again: Option = db 64 | .fluent() 65 | .select() 66 | .by_id_in(TEST_COLLECTION_NAME) 67 | .obj() 68 | .one(&my_struct1.some_id) 69 | .await?; 70 | 71 | assert_eq!(Some(object_updated), find_it_again); 72 | 73 | Ok(()) 74 | } 75 | -------------------------------------------------------------------------------- /tests/crud-integration-tests.rs: -------------------------------------------------------------------------------- 1 | use crate::common::setup; 2 | use chrono::{DateTime, Utc}; 3 | use firestore::*; 4 | use futures::stream::BoxStream; 5 | use futures::StreamExt; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | mod common; 9 | 10 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] 11 | struct MyTestStructure { 12 | some_id: String, 13 | some_string: String, 14 | one_more_string: String, 15 | some_num: u64, 16 | created_at: DateTime, 17 | } 18 | 19 | #[tokio::test] 20 | async fn crud_tests() -> Result<(), Box> { 21 | const TEST_COLLECTION_NAME: &str = "integration-test-crud"; 22 | 23 | let db = setup().await?; 24 | 25 | let my_struct1 = MyTestStructure { 26 | some_id: "test-0".to_string(), 27 | some_string: "some_string".to_string(), 28 | one_more_string: "one_more_string".to_string(), 29 | some_num: 42, 30 | created_at: Utc::now(), 31 | }; 32 | 33 | let my_struct2 = MyTestStructure { 34 | some_id: "test-1".to_string(), 35 | some_string: "some_string-1".to_string(), 36 | one_more_string: "one_more_string-1".to_string(), 37 | some_num: 17, 38 | created_at: Utc::now(), 39 | }; 40 | 41 | db.fluent() 42 | .delete() 43 | .from(TEST_COLLECTION_NAME) 44 | .document_id(&my_struct1.some_id) 45 | .execute() 46 | .await?; 47 | 48 | db.fluent() 49 | .delete() 50 | .from(TEST_COLLECTION_NAME) 51 | .document_id(&my_struct2.some_id) 52 | .execute() 53 | .await?; 54 | 55 | let object_returned: MyTestStructure = db 56 | .fluent() 57 | .insert() 58 | .into(TEST_COLLECTION_NAME) 59 | .document_id(&my_struct1.some_id) 60 | .object(&my_struct1) 61 | .execute() 62 | .await?; 63 | 64 | db.fluent() 65 | .insert() 66 | .into(TEST_COLLECTION_NAME) 67 | .document_id(&my_struct2.some_id) 68 | .object(&my_struct2) 69 | .execute::<()>() 70 | .await?; 71 | 72 | assert_eq!(object_returned, my_struct1); 73 | 74 | let object_updated: MyTestStructure = db 75 | .fluent() 76 | .update() 77 | .fields(paths!(MyTestStructure::{some_num, one_more_string})) 78 | .in_col(TEST_COLLECTION_NAME) 79 | .document_id(&my_struct1.some_id) 80 | .object(&MyTestStructure { 81 | some_num: my_struct1.some_num + 1, 82 | some_string: "should-not-change".to_string(), 83 | one_more_string: "updated-value".to_string(), 84 | ..my_struct1.clone() 85 | }) 86 | .execute() 87 | .await?; 88 | 89 | assert_eq!( 90 | object_updated, 91 | MyTestStructure { 92 | some_num: my_struct1.some_num + 1, 93 | one_more_string: "updated-value".to_string(), 94 | ..my_struct1.clone() 95 | } 96 | ); 97 | 98 | let find_it_again: Option = db 99 | .fluent() 100 | .select() 101 | .by_id_in(TEST_COLLECTION_NAME) 102 | .obj() 103 | .one(&my_struct1.some_id) 104 | .await?; 105 | 106 | assert_eq!(Some(object_updated.clone()), find_it_again); 107 | 108 | let get_both_stream: BoxStream> = Box::pin( 109 | db.batch_stream_get_objects( 110 | TEST_COLLECTION_NAME, 111 | [&my_struct1.some_id, &my_struct2.some_id], 112 | None, 113 | ) 114 | .await? 115 | .map(|(_, obj)| obj), 116 | ); 117 | 118 | let get_both_stream_vec: Vec> = get_both_stream.collect().await; 119 | 120 | assert_eq!(vec![find_it_again, Some(my_struct2)], get_both_stream_vec); 121 | 122 | Ok(()) 123 | } 124 | -------------------------------------------------------------------------------- /tests/macro_path_test.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn test_ambiguous_path_macro() { 3 | struct MyTestStructure { 4 | some_id: String, 5 | some_num: u64, 6 | } 7 | assert_eq!(firestore::path!(MyTestStructure::some_id), "some_id"); 8 | assert_eq!( 9 | firestore::paths!(MyTestStructure::{some_id, some_num}), 10 | vec!["some_id".to_string(), "some_num".to_string()] 11 | ); 12 | assert_eq!( 13 | firestore::path_camel_case!(MyTestStructure::some_id), 14 | "someId" 15 | ); 16 | assert_eq!( 17 | firestore::paths_camel_case!(MyTestStructure::{some_id, some_num}), 18 | vec!["someId".to_string(), "someNum".to_string()] 19 | ); 20 | } 21 | 22 | mod struct_path { 23 | #[macro_export] 24 | macro_rules! path { 25 | () => { 26 | unreachable!() 27 | }; 28 | } 29 | 30 | #[macro_export] 31 | macro_rules! paths { 32 | ($($x:tt)*) => {{ 33 | unreachable!() 34 | }}; 35 | } 36 | 37 | #[macro_export] 38 | macro_rules! path_camel_case { 39 | ($($x:tt)*) => {{ 40 | unreachable!() 41 | }}; 42 | } 43 | 44 | #[macro_export] 45 | macro_rules! paths_camel_case { 46 | ($($x:tt)*) => {{ 47 | unreachable!() 48 | }}; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/nested-collections-tests.rs: -------------------------------------------------------------------------------- 1 | use crate::common::setup; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | mod common; 5 | 6 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] 7 | struct MyParentStructure { 8 | some_id: String, 9 | some_string: String, 10 | } 11 | 12 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] 13 | struct MyChildStructure { 14 | some_id: String, 15 | another_string: String, 16 | } 17 | 18 | #[tokio::test] 19 | async fn crud_tests() -> Result<(), Box> { 20 | const TEST_PARENT_COLLECTION_NAME: &str = "integration-nested-test"; 21 | const TEST_CHILD_COLLECTION_NAME: &str = "integration-test-childs"; 22 | 23 | let db = setup().await?; 24 | 25 | let parent_struct = MyParentStructure { 26 | some_id: "test-parent".to_string(), 27 | some_string: "Test".to_string(), 28 | }; 29 | 30 | // Remove if it already exist 31 | db.fluent() 32 | .delete() 33 | .from(TEST_PARENT_COLLECTION_NAME) 34 | .document_id(&parent_struct.some_id) 35 | .execute() 36 | .await?; 37 | 38 | // Creating a parent doc 39 | db.fluent() 40 | .insert() 41 | .into(TEST_PARENT_COLLECTION_NAME) 42 | .document_id(&parent_struct.some_id) 43 | .object(&parent_struct) 44 | .execute::<()>() 45 | .await?; 46 | 47 | // Creating a child doc 48 | let child_struct = MyChildStructure { 49 | some_id: "test-child".to_string(), 50 | another_string: "TestChild".to_string(), 51 | }; 52 | 53 | // The doc path where we store our childs 54 | let parent_path = db.parent_path(TEST_PARENT_COLLECTION_NAME, &parent_struct.some_id)?; 55 | 56 | // Remove child doc if exists 57 | db.fluent() 58 | .delete() 59 | .from(TEST_CHILD_COLLECTION_NAME) 60 | .parent(&parent_path) 61 | .document_id(&child_struct.some_id) 62 | .execute() 63 | .await?; 64 | 65 | // Create a child doc 66 | db.fluent() 67 | .insert() 68 | .into(TEST_CHILD_COLLECTION_NAME) 69 | .document_id(&child_struct.some_id) 70 | .parent(&parent_path) 71 | .object(&child_struct) 72 | .execute::<()>() 73 | .await?; 74 | 75 | let find_parent: Option = db 76 | .fluent() 77 | .select() 78 | .by_id_in(TEST_PARENT_COLLECTION_NAME) 79 | .obj() 80 | .one(&parent_struct.some_id) 81 | .await?; 82 | 83 | assert_eq!(find_parent, Some(parent_struct)); 84 | 85 | let find_child: Option = db 86 | .fluent() 87 | .select() 88 | .by_id_in(TEST_CHILD_COLLECTION_NAME) 89 | .parent(&parent_path) 90 | .obj() 91 | .one(&child_struct.some_id) 92 | .await?; 93 | 94 | assert_eq!(find_child, Some(child_struct)); 95 | 96 | Ok(()) 97 | } 98 | -------------------------------------------------------------------------------- /tests/query-integration-tests.rs: -------------------------------------------------------------------------------- 1 | use crate::common::setup; 2 | use chrono::prelude::*; 3 | use futures::stream::BoxStream; 4 | use futures::StreamExt; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | mod common; 8 | use firestore::*; 9 | 10 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] 11 | struct MyTestStructure { 12 | some_id: String, 13 | some_string: String, 14 | one_more_string: String, 15 | some_num: u64, 16 | created_at: DateTime, 17 | } 18 | 19 | #[tokio::test] 20 | async fn crud_tests() -> Result<(), Box> { 21 | const TEST_COLLECTION_NAME: &str = "integration-test-query"; 22 | 23 | let db = setup().await?; 24 | 25 | let my_struct1 = MyTestStructure { 26 | some_id: "test-0".to_string(), 27 | some_string: "some_string".to_string(), 28 | one_more_string: "one_more_string".to_string(), 29 | some_num: 42, 30 | created_at: Utc::now(), 31 | }; 32 | 33 | let my_struct2 = MyTestStructure { 34 | some_id: "test-1".to_string(), 35 | some_string: "some_string-1".to_string(), 36 | one_more_string: "one_more_string-1".to_string(), 37 | some_num: 17, 38 | created_at: Utc::now(), 39 | }; 40 | 41 | db.fluent() 42 | .delete() 43 | .from(TEST_COLLECTION_NAME) 44 | .document_id(&my_struct1.some_id) 45 | .execute() 46 | .await?; 47 | 48 | db.fluent() 49 | .delete() 50 | .from(TEST_COLLECTION_NAME) 51 | .document_id(&my_struct2.some_id) 52 | .execute() 53 | .await?; 54 | 55 | db.fluent() 56 | .insert() 57 | .into(TEST_COLLECTION_NAME) 58 | .document_id(&my_struct1.some_id) 59 | .object(&my_struct1) 60 | .execute::<()>() 61 | .await?; 62 | 63 | db.fluent() 64 | .insert() 65 | .into(TEST_COLLECTION_NAME) 66 | .document_id(&my_struct2.some_id) 67 | .object(&my_struct2) 68 | .execute::<()>() 69 | .await?; 70 | 71 | let object_stream: BoxStream = db 72 | .fluent() 73 | .select() 74 | .from(TEST_COLLECTION_NAME) 75 | .filter(|q| { 76 | q.for_all([ 77 | q.field(path!(MyTestStructure::some_num)).is_not_null(), 78 | q.field(path!(MyTestStructure::some_string)) 79 | .eq("some_string"), 80 | ]) 81 | }) 82 | .order_by([( 83 | path!(MyTestStructure::some_num), 84 | FirestoreQueryDirection::Descending, 85 | )]) 86 | .obj() 87 | .stream_query() 88 | .await?; 89 | 90 | let objects_as_vec1: Vec = object_stream.collect().await; 91 | assert_eq!(objects_as_vec1, vec![my_struct1.clone()]); 92 | 93 | let object_stream: BoxStream = db 94 | .fluent() 95 | .select() 96 | .from(TEST_COLLECTION_NAME) 97 | .filter(|q| { 98 | q.for_any([ 99 | q.field(path!(MyTestStructure::some_string)) 100 | .eq("some_string"), 101 | q.field(path!(MyTestStructure::some_string)) 102 | .eq("some_string-1"), 103 | ]) 104 | }) 105 | .order_by([( 106 | path!(MyTestStructure::some_num), 107 | FirestoreQueryDirection::Descending, 108 | )]) 109 | .obj() 110 | .stream_query() 111 | .await?; 112 | 113 | let objects_as_vec2: Vec = object_stream.collect().await; 114 | assert_eq!(objects_as_vec2, vec![my_struct1, my_struct2]); 115 | 116 | Ok(()) 117 | } 118 | -------------------------------------------------------------------------------- /tests/transaction-tests.rs: -------------------------------------------------------------------------------- 1 | use crate::common::setup; 2 | use serde::{Deserialize, Serialize}; 3 | use std::sync::atomic::{AtomicUsize, Ordering}; 4 | use std::sync::Arc; 5 | 6 | mod common; 7 | use firestore::*; 8 | 9 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] 10 | struct MyTestStructure { 11 | some_id: String, 12 | some_string: String, 13 | } 14 | 15 | #[tokio::test] 16 | async fn transaction_tests() -> Result<(), Box> { 17 | let db = setup().await?; 18 | 19 | const TEST_COLLECTION_NAME: &str = "integration-test-transactions"; 20 | 21 | let my_struct = MyTestStructure { 22 | some_id: "test-1".to_string(), 23 | some_string: "Test".to_string(), 24 | }; 25 | 26 | db.fluent() 27 | .delete() 28 | .from(TEST_COLLECTION_NAME) 29 | .document_id(&my_struct.some_id) 30 | .execute() 31 | .await?; 32 | 33 | let object_created: MyTestStructure = db 34 | .fluent() 35 | .update() 36 | .in_col(TEST_COLLECTION_NAME) 37 | .precondition(FirestoreWritePrecondition::Exists(false)) 38 | .document_id(&my_struct.some_id) 39 | .object(&my_struct.clone()) 40 | .execute() 41 | .await?; 42 | 43 | assert_eq!(object_created, my_struct); 44 | 45 | { 46 | let transaction = db.begin_transaction().await?; 47 | let db = db.clone_with_consistency_selector(FirestoreConsistencySelector::Transaction( 48 | transaction.transaction_id.clone(), 49 | )); 50 | db.fluent() 51 | .select() 52 | .by_id_in(TEST_COLLECTION_NAME) 53 | .obj::() 54 | .one(&my_struct.some_id) 55 | .await?; 56 | transaction.commit().await?; 57 | } 58 | 59 | { 60 | let transaction = db.begin_transaction().await?; 61 | let db = db.clone_with_consistency_selector(FirestoreConsistencySelector::Transaction( 62 | transaction.transaction_id.clone(), 63 | )); 64 | let object_updated: MyTestStructure = db 65 | .fluent() 66 | .update() 67 | .in_col(TEST_COLLECTION_NAME) 68 | .precondition(FirestoreWritePrecondition::Exists(true)) 69 | .document_id(&my_struct.some_id) 70 | .object(&my_struct.clone()) 71 | .execute() 72 | .await?; 73 | transaction.commit().await?; 74 | assert_eq!(object_updated, my_struct); 75 | } 76 | 77 | // Handling permanent errors 78 | { 79 | let res: FirestoreResult<()> = db 80 | .run_transaction(|_db, _tx| { 81 | Box::pin(async move { 82 | //Test returning an error 83 | Err(backoff::Error::Permanent(common::CustomUserError::new( 84 | "test error", 85 | ))) 86 | }) 87 | }) 88 | .await; 89 | assert!(res.is_err()); 90 | } 91 | 92 | // Handling transient errors 93 | { 94 | let counter = Arc::new(AtomicUsize::new(1)); 95 | let res: FirestoreResult<()> = db 96 | .run_transaction(|_db, _tx| { 97 | let counter = counter.fetch_add(1, Ordering::Relaxed); 98 | Box::pin(async move { 99 | if counter > 2 { 100 | return Ok(()); 101 | } 102 | //Test returning an error 103 | Err(backoff::Error::Transient { 104 | err: common::CustomUserError::new("test error"), 105 | retry_after: None, 106 | }) 107 | }) 108 | }) 109 | .await; 110 | assert!(res.is_ok()); 111 | } 112 | 113 | Ok(()) 114 | } 115 | -------------------------------------------------------------------------------- /tests/transform_tests.rs: -------------------------------------------------------------------------------- 1 | use crate::common::setup; 2 | use firestore::*; 3 | 4 | mod common; 5 | 6 | #[tokio::test] 7 | async fn crud_tests() -> Result<(), Box> { 8 | const TEST_COLLECTION_NAME: &str = "integration-test-transform"; 9 | 10 | let db = setup().await?; 11 | 12 | db.fluent() 13 | .delete() 14 | .from(TEST_COLLECTION_NAME) 15 | .document_id("test-t0") 16 | .execute() 17 | .await?; 18 | 19 | db.fluent() 20 | .insert() 21 | .into(TEST_COLLECTION_NAME) 22 | .document_id("test-t0") 23 | .document(FirestoreDb::serialize_map_to_doc( 24 | "", 25 | [( 26 | "bar", 27 | FirestoreValue::from_map([("123", ["inner-value"].into())]), 28 | )], 29 | )?) 30 | .execute() 31 | .await?; 32 | 33 | let mut transaction = db.begin_transaction().await?; 34 | 35 | db.fluent() 36 | .update() 37 | .in_col(TEST_COLLECTION_NAME) 38 | .document_id("test-t0") 39 | .transforms(|t| t.fields([t.field("bar.`123`").append_missing_elements(["987654321"])])) 40 | .only_transform() 41 | .add_to_transaction(&mut transaction)?; 42 | 43 | transaction.commit().await?; 44 | 45 | let doc_returned = db 46 | .fluent() 47 | .select() 48 | .by_id_in(TEST_COLLECTION_NAME) 49 | .one("test-t0") 50 | .await?; 51 | 52 | assert_eq!( 53 | doc_returned.map(|d| d.fields), 54 | Some( 55 | FirestoreDb::serialize_map_to_doc( 56 | "", 57 | [( 58 | "bar", 59 | FirestoreValue::from_map([("123", ["inner-value", "987654321"].into())]), 60 | )], 61 | )? 62 | .fields 63 | ) 64 | ); 65 | 66 | Ok(()) 67 | } 68 | -------------------------------------------------------------------------------- /tests/update-precondition-test.rs: -------------------------------------------------------------------------------- 1 | use crate::common::setup; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | mod common; 5 | use firestore::*; 6 | 7 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] 8 | struct MyTestStructure { 9 | some_id: String, 10 | some_string: String, 11 | } 12 | 13 | #[tokio::test] 14 | async fn precondition_tests() -> Result<(), Box> { 15 | let db = setup().await?; 16 | 17 | const TEST_COLLECTION_NAME: &str = "integration-test-precondition"; 18 | 19 | let my_struct = MyTestStructure { 20 | some_id: "test-1".to_string(), 21 | some_string: "Test".to_string(), 22 | }; 23 | 24 | db.fluent() 25 | .delete() 26 | .from(TEST_COLLECTION_NAME) 27 | .document_id(&my_struct.some_id) 28 | .execute() 29 | .await?; 30 | 31 | let should_fail: FirestoreResult<()> = db 32 | .fluent() 33 | .update() 34 | .in_col(TEST_COLLECTION_NAME) 35 | .precondition(FirestoreWritePrecondition::Exists(true)) 36 | .document_id(&my_struct.some_id) 37 | .object(&MyTestStructure { 38 | some_string: "created-value".to_string(), 39 | ..my_struct.clone() 40 | }) 41 | .execute() 42 | .await; 43 | 44 | assert!(should_fail.is_err()); 45 | 46 | let object_created: MyTestStructure = db 47 | .fluent() 48 | .update() 49 | .in_col(TEST_COLLECTION_NAME) 50 | .precondition(FirestoreWritePrecondition::Exists(false)) 51 | .document_id(&my_struct.some_id) 52 | .object(&my_struct.clone()) 53 | .execute() 54 | .await?; 55 | 56 | assert_eq!(object_created, my_struct); 57 | 58 | let object_updated: MyTestStructure = db 59 | .fluent() 60 | .update() 61 | .in_col(TEST_COLLECTION_NAME) 62 | .precondition(FirestoreWritePrecondition::Exists(true)) 63 | .document_id(&my_struct.some_id) 64 | .object(&my_struct.clone()) 65 | .execute() 66 | .await?; 67 | 68 | assert_eq!(object_updated, my_struct); 69 | 70 | Ok(()) 71 | } 72 | --------------------------------------------------------------------------------