Firestore Façade
A clean, strongly-typed, zero-dependency API for Firestore Typescript projects.
NOTE: This project is still in its infancy, and probably not ready for
consumption just yet
Motivation
- Reduce boilerplate code and improve readability
- Provide strict typing on collection methods like
set
and update
- Provide strict typing on query select statements and their partial response
documents
The aim is to keep the API as close to native as possible while providing as
much type safety as is still practical. Firestore Façade is a fairly thin
wrapper which does not prevent you from using using plain Firestore API methods.
For that reason it is also not aiming to cover the full Firestore API surface.
At the moment this library focusses solely on Node.js using the
firestore-admin client.
It should be possible to make this compatible with the Cloud
Firestore with a small
modification, because both are technically the same product.
In the future I would like to add a package addressing the web client and
another one specifically for React hooks.
Usage
1. Install
npm install firestore-facade
npm install firestore-facade-cli ts-node --save-dev
Or, if you prefer Yarn:
yarn add firestore-facade
yarn add firestore-facade-cli ts-node --save-dev
Currently ts-node is required because the generate script tries to resolve the
ts-node loader from the environment where you call the command. I hope find a
way to make the command self-contained in the future.
2. Configure Document Type Mapping
In your repository, create a configuration file. It can be named anything and
placed anywhere. In this file you create a default export object using a
root
and optionally a sub
property.
In the root property, list all the Firestore root collections using the name as
key, and apply their document type on a placeholder object as shown below:
export default {
root: {
athletes: {} as Athlete,
sports_events: {} as Event,
},
sub: {
athletes: {
medals: {} as Medal,
},
},
};
Subcollections are defined under sub
. Currently one level of nesting is
supported.
In this example the collection names in Firestore are snake-cased, but if your
names are camel-cased the key names should mirror that.
The empty objects are only there to make the types available at runtime.
@TODO explain in more detail.
3. Generate Facade Factory Function
The next step is to generate the facade code by passing the location of the
configuration file to the generate-facade
command:
npx generate-facade ./src/my-document-types-config.ts
Or if you use Yarn:
yarn run generate-facade ./src/my-document-types-config.ts
This should create a file named facade.ts
containing the facade factory
function in the same location as the supplied config file.
Whenever you change something about your collections or their document types,
simply re-run this command to update the facade function.
4. Wrap Firestore Instance
Now you can use the facade factory to wrap your instance of the Firestore
client.
import { createFacade } from "./facade";
const db = createFacade(firestore);
API
Below is an example showing the different API methods.
You can find the complete source code in the nodejs example
package
@TODO document API in detail.
const ref = await db.athletes.add({
name: "Joe",
age: 23,
skills: { c: true, d: ["one", "two", "three"], tuple: ["foo", 123] },
});
console.log(`Stored new document at collection_a/${ref.id}`);
await db.athletes.set(ref.id, {
name: "Jane",
age: 26,
skills: { c: true, d: ["one", "two", "three"], tuple: ["foo", 456] },
});
await db.athletes.update(ref.id, {
age: 27,
"skills.c": true,
"skills.tuple": ["bar", 890],
updated_at: serverTimestamp(),
});
const doc = await db.athletes.get(ref.id);
console.log(doc.data);
const { id: eventId } = await db.sports_events.add({
name: "Olympics",
year: 2045,
});
await db.athletes.sub(ref.id).medals.add({
event_id: eventId,
type: "gold",
});
const fullDocs = await db.athletes.query((ref) =>
ref.where("skills.c", "==", true),
);
console.log(`Retrieved ${fullDocs.length} documents`);
const partialDocs = await db.athletes.queryAndSelect(
(ref) => ref.where("updated_at", "<", new Date()),
["name", "skills"],
);
partialDocs.forEach((doc) => console.log(doc.data.name, doc.data.skills));