Typesafe repositories around Firestore providing a straightforward API to read and write documents.
npm i firestore-storage-core firestore-storage
import { BaseModel } from 'firebase-storage-core';
import { initializeApp } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';
initializeApp();
// restaurants/{restaurantId}
const restaurantRepo = new RestaurantRepository(getFirestore());
const restaurant = await restaurantRepo.save({
name: 'FreshFoods',
type: 'vegan'
});
console.log(restaurant);
/*
{
id: '0vdxYqEisf5vwJLhyLjA',
name: 'FreshFoods',
_rawPath: 'restaurants/0vdxYqEisf5vwJLhyLjA'
}*/
// Query restaurants based on properties
await restaurantRepo.list({
type: 'vegan'
});
// More complex queries
await restaurantRepo.query((qb) => {
return qb
.where((r) => r.type, '==', 'steak')
.where((r) => r.address.city, '==', 'NY')
});
The properties id
and _rawPath
from BaseModel
are dynamically added during reads
and removed before writes.
When working with nested collections, read and write methods require a parameter to supply a map of all parent document ids
// restaurants/{restaurantId}/reviews/{reviewId}
const reviewRepo = new ReviewRepository(getFirestore());
const review = await reviewRepo.save({
userId: 'my-user-uid-123',
stars: 5
}, {
restaurantId: '0vdxYqEisf5vwJLhyLjA'
});
console.log(review);
/*
{
id: 'a393f73b884c4a0981c0',
userId: 'my-user-uid-123',
stars: 5
_rawPath: 'restaurants/0vdxYqEisf5vwJLhyLjA/reviews/a393f73b884c4a0981c0'
}*/
Create repository classes for each collection you want to query documents from. For example,
if you want to query documents to query from the users
collection you create a class UserRepository
extending BaseRepository
.
Each repository provides a list of functions for saving, querying and deleting documents,
and you can extend each repository based on your needs.
export namespace Collections {
// To define restaurants/{restaurantId}.
export const Restaurants = new CollectionPath(
// Name of the collection
'restaurants',
// Template variable name and property name on the id map
'restaurantId');
// When defining nested collections a few generics are required
// restaurants/{restaurantId}/reviews/{reviewId}
export const Restaurants_Reviews = new CollectionPath<
// Template variable
'reviewId',
// Type of the id on the model
string,
// Type of the id map from the parent collection
DocumentIds<typeof Restaurants>
>(
// Name of the collection
'reviews',
// Template variable name and property name on the id map
'reviewId',
// Path of the parent collection
Restaurants
);
}
// Path to document: restaurants/0vdxYqEisf5vwJLhyLjA/reviews/a393f73b884c4a0981c0
Collections.Restaurants_Reviews.doc({
restaurantId: '0vdxYqEisf5vwJLhyLjA',
reviewId: 'a393f73b884c4a0981c0'
})
// Path to collection: restaurants/0vdxYqEisf5vwJLhyLjA/reviews
Collections.Restaurants_Reviews.collection({
restaurantId: '0vdxYqEisf5vwJLhyLjA'
})
// Path template: restaurants/{restaurantId}/reviews/{reviewId}
Collections.Restaurants_Reviews.path();
// Parse ids from path
Collections.Restaurants_Reviews.parse(
'restaurants/0vdxYqEisf5vwJLhyLjA/reviews/a393f73b884c4a0981c0'
);
/**
* {
* restaurantId: '0vdxYqEisf5vwJLhyLjA',
* reviewId: 'a393f73b884c4a0981c0'
* }
*/
import { BaseRepository } from 'firestore-storage';
import { Repository } from 'firestore-storage-core';
interface Review {
userId: string;
stars: number;
}
@Repository({
path: Collections.Restaurants_Reviews
})
export class ReviewRepository extends BaseRepository<Review, typeof Collections.Restaurants_Reviews> {
constructor() {
super(getFirestore());
}
}
find*()
methods return the document or null when no result was foundget*()
methods always return the document and will throw an error when no result was foundlist*()
methods always return an array and never null. When no result is found, the array is empty