#firestore #google-cloud #document-database #gcp #key-value-store #collection #serialize-deserialize

tiny-firestore-odm

A tiny object-document mapper for Google Firestore, focusing on a key/value object store usage model

8 releases

0.2.6 Oct 23, 2021
0.2.5 Oct 22, 2021
0.1.0 Oct 12, 2021

#2653 in Database interfaces

MIT/Apache

45KB
860 lines

tiny-firestore-odm

wokflow state crates.io docs.rs

tiny-firestore-odm is a lightweight Object Document Mapper for Firestore. It's built on top of 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? for a compatible Rust/GCP stack.

Usage

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.

Method Behavior if object exists Behavior if object does not exist
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.

Dependencies

~65MB
~1M SLoC