# `tiny-firestore-odm`
[![wokflow state](https://github.com/paulgb/tiny-firestore-odm/workflows/Rust/badge.svg)](https://github.com/paulgb/tiny-firestore-odm/actions/workflows/rust.yml)
[![crates.io](https://img.shields.io/crates/v/tiny-firestore-odm.svg)](https://crates.io/crates/tiny-firestore-odm)
[![docs.rs](https://img.shields.io/badge/docs-release-brightgreen)](https://docs.rs/tiny-firestore-odm/)
`tiny-firestore-odm` is a lightweight Object Document Mapper for Firestore. It's built on top of
[`firestore-serde`](https://github.com/paulgb/firestore-serde) (which does the
document/object translation), and adds a Rust representation of Firestore *collections* along with
methods to create/modify/delete from them.
The intent is not to provide access to all of Firestore's functionality, but to provide a
simplified interface centered around using Firestore as a key/value store for arbitrary
collections of (serializable) Rust objects.
See [Are We Google Cloud Yet?](https://github.com/paulgb/are-we-google-cloud-yet) for a compatible Rust/GCP stack.
## Usage
```rust
use google_authz::Credentials;
use tiny_firestore_odm::{Collection, Database, NamedDocument};
use serde::{Deserialize, Serialize};
use tokio_stream::StreamExt;
// Define our data model.
// Any Rust type that implements Serialize and Deserialize can be stored in a Collection.
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct ActorRole {
actor: String,
role: String,
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Movie {
pub name: String,
pub year: u32,
pub runtime: u32,
pub cast: Vec<ActorRole>,
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
// Use `google-authz` for credential discovery.
let creds = Credentials::default().await;
// Firestore databases are namespaced by project ID, so we need that too.
let project_id = std::env::var("GCP_PROJECT_ID").expect("Expected GCP_PROJECT_ID env var.");
// A Database is the main wrapper around a raw FirestoreClient.
// It gives us a way to create Collections.
let database = Database::new(creds.into(), &project_id).await;
// A Collection is a reference to a Firestore collection, combined with a type.
let movies: Collection<Movie> = database.collection("tiny-firestore-odm-example-movies");
// Construct a movie to insert into our collection.
let movie = Movie {
name: "The Big Lebowski".to_string(),
year: 1998,
runtime: 117,
cast: vec![
ActorRole {
actor: "Jeff Bridges".to_string(),
role: "The Dude".to_string(),
},
ActorRole {
actor: "John Goodman".to_string(),
role: "Walter Sobchak".to_string(),
},
ActorRole {
actor: "Julianne Moore".to_string(),
role: "Maude Lebowski".to_string(),
},
]
};
// Save the movie to the collection. When we insert a document with `create`, it is assigned
// a random key which is returned to us if it is created successfully.
let movie_id = movies.create(&movie).await.unwrap();
// We can use the key that was returned to fetch the film.
let movie_copy = movies.get(&movie_id).await.unwrap();
assert_eq!(movie, movie_copy);
// Alternatively, we can supply a string to use as the key, like this:
movies.try_create(&movie, "The Big Lebowski").await.unwrap();
// Then, we can retrieve it with the same string.
let movie_copy2 = movies.get("The Big Lebowski").await.unwrap();
assert_eq!(movie, movie_copy2);
// To clean up, let's loop over documents in the collection and delete them.
let mut result = movies.list();
// List returns a `futures_core::Stream` of `NamedDocument` objects.
while let Some(NamedDocument {name, ..}) = result.next().await {
movies.delete(&name).await.unwrap();
}
}
```
## Document Existence Semantics
Different methods are provided to achieve different semantics around what to do if the document
does or doesn't exist, summarized in the table below.
| `create` | N/A (picks new key) | Create |
| `create_with_key` | Error | Create |
| `try_create` | Do nothing; return `Ok(false)` | Create; return `Ok(true)` |
| `upsert` | Replace | Create |
| `update` | Replace | Error |
| `delete` | Delete | Error |
## Limitations
This crate is designed for workflows that treat Firestore as a key/value store, with each
collection corresponding to one Rust type (though one Rust type may correspond to multiple
Firestore collections).
It currently does not support functionality outside of that, including:
- Querying by anything except key
- Updating only part of a document
- Transactions
- Subscribing to updates
(I haven't ruled out supporting any of those features, but the goal is crate is not to
comprehensively support all GCP features, just a small but useful subset.)
## Running tests
The unit tests in this crate can be run without any special setup. To do so, run:
cargo test --lib
There are also integration tests that test the functionality of interacting with the
outside world. To use these, you must provide Google Cloud credentials. I recommend
creating a Google Cloud project specifically for integration tests, since Firestore
is namespaced by project and it avoids the integration tests writing to a database
used for other things.
Then, set two environment variables:
- `GOOGLE_APPLICATION_CREDENTIALS`, containing the absolute path of a `.json` file on
disk which contains a service account credentials file. You can download this file
for a service account through the Google Cloud Console.
- `GCP_PROJECT_ID`, containing the name of the project whose Firebase you would like
to use. This is usually the same as the `project_id` field of the service account
JSON file.
With these set, you can run:
cargo test
to run all unit and integration tests.