All Projects → freshfox → firestore-storage

freshfox / firestore-storage

Licence: MIT license
A typed wrapper around Firestore incluing a querybuilder and an in-memory implementation for testing

Programming Languages

typescript
32286 projects

Projects that are alternatives of or similar to firestore-storage

firebase-bundle
A Symfony Bundle for the Firebase PHP Admin SDK
Stars: ✭ 112 (+314.81%)
Mutual labels:  firestore
laravel-repository-pattern
Files autogenerator for repositorry pattern
Stars: ✭ 46 (+70.37%)
Mutual labels:  repository-pattern
firecms
Awesome Firebase/Firestore-based CMS. The missing admin panel for your Firebase project!
Stars: ✭ 686 (+2440.74%)
Mutual labels:  firestore
clean-architecture-azure-cosmos-db
A starting point to build a web API to work with Azure Cosmos DB using .NET 5 and Azure Cosmos DB .NET SDK V3, based on Clean Architecture and repository design pattern. Partition key is also implemented through the repository pattern.
Stars: ✭ 277 (+925.93%)
Mutual labels:  repository-pattern
FoodApp
Proof of concept for food app [JetPack + Coroutines + Flow + MockK + JaCoCo coverage + SonarQube]
Stars: ✭ 25 (-7.41%)
Mutual labels:  repository-pattern
scheduler
Laravel on my way
Stars: ✭ 20 (-25.93%)
Mutual labels:  repository-pattern
JewelCase
This is the source code for JewelCase, a sample app demonstrating how to use SwiftUI and Firebase together. This slide deck discusses the architecture of the app: https://www.slideshare.net/peterfriese/building-swiftui-apps-with-firebase
Stars: ✭ 42 (+55.56%)
Mutual labels:  firestore
Simple-Notes-Kotlin-App
✍️ Simple Note Making App use mvvm architecture , dagger , coroutines and navigation component. Features includes 🗒️ create , edit and ❌ delete notes
Stars: ✭ 40 (+48.15%)
Mutual labels:  repository-pattern
foundry-cli
Foundry makes the development of Firebase Functions fast by giving you an out-of-the-box working cloud environment for your development with an access to your production data. It's a CLI tool that gives you a continuous REPL-like feedback about your Firebase Functions.
Stars: ✭ 49 (+81.48%)
Mutual labels:  firestore
Building-a-Node-Express.js-Rest-API-server-using-a-repository-pattern
A complete example of a Node + Express.js rest api server
Stars: ✭ 45 (+66.67%)
Mutual labels:  repository-pattern
go-firestorm
Simple Go ORM for Google/Firebase Cloud Firestore
Stars: ✭ 39 (+44.44%)
Mutual labels:  firestore
vue-blog
Book blog
Stars: ✭ 31 (+14.81%)
Mutual labels:  firestore
demo-firebase-js
A simple Web application that demonstrates how the end-to-end encryption works. The application uses firebase as a backend service for authentication and chat messaging, and Virgil E3Kit SDK for end-to-end encryption.
Stars: ✭ 31 (+14.81%)
Mutual labels:  firestore
godot-android-plugin-firebase
Godot 3.2.2 Android plugin for Firebase
Stars: ✭ 41 (+51.85%)
Mutual labels:  firestore
wordsreminder
React native application to save words in dictionaries.
Stars: ✭ 33 (+22.22%)
Mutual labels:  firestore
AngularPos
A real-time, simple web Point of Sale system written with Angular 12, Firebase (Cloud Firestore), Bootstrap 4 and PrimeNg
Stars: ✭ 67 (+148.15%)
Mutual labels:  firestore
vue-js-3-firebase-firestore
Vue 3 Firebase Tutorial: Build Firestore CRUD Web Application
Stars: ✭ 34 (+25.93%)
Mutual labels:  firestore
pring-admin.ts
Cloud Firestore model framework for TypeScript - Google
Stars: ✭ 13 (-51.85%)
Mutual labels:  firestore
Manime
🍱 An anime app, based on single activity and MVVM architecture.
Stars: ✭ 24 (-11.11%)
Mutual labels:  repository-pattern
react-firebase-context
A basic set of components that help dealing with Firebase services
Stars: ✭ 41 (+51.85%)
Mutual labels:  firestore

Firestore Storage

Build Status

Table of Contents

Overview

Typed repositories for Node around Firestore providing a very simple API to write and read documents. Including a simple to use query builder and an in-memory storage implementation for running blazing fast tests

Firestore Storage provides a thin layer of abstraction to accessing data in Firestore. It follows the repository pattern, for more information about it you can read this short article

Return value conventions for methods

  • find*() methods return the document or null when no result was found
  • get*() methods always return the document and will throw an error when no result was found
  • list*() methods always return an array and never null. When no result is found the array is empty

Example

const restaurantRepo = new RestaurantRepository();

// Saving data
const restaurant = await restaurantRepo.save({
  name: 'FreshFoods',
  address: 'SomeStreet 123',
  city: 'New York',
  type: 'vegan'
});

console.log(restaurant);
/*
{
  id: '0vdxYqEisf5vwJLhyLjA',
  name: 'FreshFoods',
  address: 'SomeStreet 123',
  city: 'New York',
  type: 'vegan',
  createdAt: Date('2019-04-29T16:35:33.195Z'),
  updatedAt: Date('2019-04-29T16:35:33.195Z')
}*/

// Listing all documents
const allRestaurants = await restaurantRepo.list();

// Filtering documents based on attributes
const restaurantsInNewYork = await restaurantRepo.list({
  city: 'New York'
});

// More complex queries
const date = new Date('2019-02-01');
const restaurants = await restaurantRepo.query((qb) => {
  return qb
    .where('openDate', '<=', date)
    .orderBy('openDate', 'asc');
});

Installation

The firestore-storage package is available via npm

$ npm install firestore-storage
# or
$ yarn add firestore-storage

Tests

To run all tests using only the MemoryStorage implementation run:

$ yarn test

It's also possible to run all tests using the FirestoreStorage implementation. To do this you need to create a Firebase project and download the Admin SDK credentials file. Copy the .env.sample to .env and add the absolute path to the FIREBASE_CREDENTIALS variable. To execute the tests run:

$ yarn test:firestore

To run tests using both MemoryStorage and FirestoreStorage run

$ yarn test

Running tests on your firestore.rules

This packages provides utilities to run tests against your Firestore rules. You need the @firebase/testing package and the local emulator installed. To install the emulator run $ firebase setup:emulators:firestore

Below is an example of how to run tests against the rules. Create a new instance of FirestoreRuleTest for each test. Add some test data, load the rules and run your assertions. The constructor of FirestoreRuleTest takes the uid of the authenticated user as an argument. This will be the request.auth.uid property which you can read in your rules. Passing no uid will send unauthenticated requests to the emulator.

import {FirestoreRuleTest} from 'firestore-storage';
import * as firebase from "@firebase/testing";

describe('Rules', function () {

	const pathToRules = `${__dirname}/../../../../firestore.rules`;

	before(async () => {
		await FirestoreRuleTest.start();
	});

	after(async () => {
		await FirestoreRuleTest.stop();
	});

	describe('Unauthenticated', function () {

		it('should not be able to read from users', async () => {
			const tc = new FirestoreRuleTest();
			const userId = 'alice';
			const userDoc = tc.firestore.collection('users').doc(userId);
			await userDoc.set({});
			await tc.loadRules(pathToRules);
			await firebase.assertFails(userDoc.get())
		});
	});

	describe('Authenticated', function () {

		it('should not be able to read reservations from different account', async () => {

			const userId1 = 'alice';
			const accountId1 = `account-${userId1}`;
			const userId2 = 'bob';
			const accountId2 = `account-${userId2}`;


			const tc = new FirestoreRuleTest(userId1);
			const userDoc1 = tc.firestore.collection('users').doc(userId1);
			const userDoc2 = tc.firestore.collection('users').doc(userId2);

			await userDoc1.set({accountId: accountId1});
			await userDoc2.set({accountId: accountId2});

			const resColl1 = tc.firestore.collection('accounts').doc(accountId1).collection('reservations');
			const resColl2 = tc.firestore.collection('accounts').doc(accountId2).collection('reservations');

			await resColl1.add({});
			await resColl2.add({});

			await tc.loadRules(pathToRules);

			await firebase.assertSucceeds(resColl1.get());
			await firebase.assertFails(resColl2.get());

		});

	});

});

Usage

Firestore Storage can be used with the dependency injection library Inversify as well as without it.

// In another file (storage.ts)
export const storage = new FirestoreStorage(admin.firestore());
// OR
export const storage = new MemoryStorage();

// restaurant_repository.ts
import {storage} from './storage';

class RestaurantRepository extends BaseRepository<Restaurant> {

  constructor() {
    super(storage);
  }

  getCollectionPath(...documentIds: string[]): string {
    return 'restaurants';
  }

  listVegan() {
    return this.list({type: 'vegan'});
  }
}

const repo = new RestaurantRepository();

Inversify

This library fully supports Inversify To use it you have load the FirestoreStorageModule module into your container and add the @injectable() decorator to each repository class. This will automatically inject the correct storage implementation (Firestore or In-Memory) into your repositories

if (process.env.NODE_ENV === 'test') {
  container.load(FirestoreStorageModule.createWithMemoryStorage());
} else {
  container.load(FirestoreStorageModule.createWithFirestore(admin.firestore()));
}

container.bind(RestaurantRepository).toSelf().inSingletonScope();

Models

Your models should extend or implement the interface BaseModel which contains the id and modification timestamps.

interface BaseModel {
  id?: string;
  createdAt?: Date;
  updatedAt?: Date;
}

Since those values are not in the document itself, they will be added to the returning object when reading from Firestore. You can pass objects with those attributes to the save() function. They will always be omitted and the id will be used as the document id when writing data.

Transactions

Each repository as well as the FirestoreStorage and MemoryStorage implementations provide a transaction() function.

Repositories

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.

When extending BaseRepository you have to implement the function getCollectionPath(...ids: string[]). For root collections the ids[] will be empty. For sub-collections this parameter will contain an hierarchically ordered list of parent document ids.

Each function takes multiple ids as its last arguments. Those are the hierarchically ordered list of parent document ids passed to the getCollectionPath(...) function.

The following examples are based on the RestaurantRepository and ReviewRepository created below

findById

Takes a hierarchical ordered list of document ids. Returns the document when found or null

const review = await reviewRepo.findById(restaurantId, reviewId);

find

Queries the collection to match the given arguments, returns the first result or null if none is found.

const review = await reviewRepo.find({
  rating: 5
}, restaurantId);

getById

Works exactly like findById but throws an error if no document was found

get

Works exactly like find but throws an error if no document was found

list

Query a list of documents with a set of given arguments. This function always returns an array. If no results were found the array will be empty

const allOneStarRatings = await reviewRepo.list({
  rating: 1
}, restaurantId);

query

Do more complex queries like greater than and lower than comparisons.

const reviews = await reviewRepo.query(() => {
  return qb
    .where('rating', '==', 2)
    .where('submitDate', '<', new Date('2019-12-31'));
}, restaurantId);

Valid operators are == | < | <= | > | >=

QueryBuilder functions

qb.where(fieldName, operator, value)
qb.orderBy(fieldName, direction) // 'asc' or 'desc'
qb.offset(number)
qb.limit(number)

findAll

Returns an array of documents for a given array of ids. The array will contain null values if some documents aren't found

const r = await restaurantRepo.findAll([id1, id2]);

getAll

Returns an array of documents for a given array of ids. The array won't contain null values. If a document doesn't exists, an error will be thrown

const r = await restaurantRepo.getAll([id1, id2]);

save

Saves a document into Firestore.

const restaurant = await restaurantRepo.save({
  name: 'Ebi'
});

If you want to update data you just have to pass the id of the document.

const user = await restaurantRepo.save({
  id: '8zCW4UszD0wmdrpBNswp',
  name: 'Ebi',
  openDate: new Date()
});

By default this will create the document with this id if it doesn't exist or merge the properties into the existing document. If you want to write a document and instead of don't merge use the [write()][write] function

write

Sets the passed data. If the document exists it will be overwritten.

const user = await restaurantRepo.write({
  name: 'FreshBurgers',
  openDate: new Date()
});

delete

Deletes a document by a given id

// For a nested collection
await reviewRepo.delete(restaurantId, reviewId);
// For a root level collection
await restaurantRepo.delete(restaurantId);

transaction

Takes an update function and an array of ids. Find more about transactions at the Firestore documentation

const result = await restaurantRepo.transaction((trx) => {
	const u = trx.get('some-id');
	u.name = 'Burger Store';
	trx.set(u);
	return 'done';
})

Extending BaseRepository

export class RestaurantRepository extends BaseRepository<Restaurant> {

	getCollectionPath(...documentIds: string[]): string {
		return 'restaurants';
	}
}

When creating repositories for nested collection it's always a good idea to check if the correct ids are passed into getCollectionPath(...).

export class ReviewRepository<T> extends BaseRepository<Review> {

  getCollectionPath(...documentIds): string {
    const id = documentIds.shift();
    if (!id) {
      throw new Error('RestaurantId id is missing');
    }
    return `restaurants/${id}/reviews`;
  }
}

This will throw an error when trying to save or query without passing the user id.

await reviewRepo.save({...}); // Throws and error
await reviewRepo.save({...}, '<restaurantId>'); // Succeeds

Migrations

This package provides a base class to migrate data in Firestore. For more info look at this example

Typing indexes

Use the IndexManager class to build your index structure and the provided fss script to generate the firestore.indexes.json. Look at src/test/storage/index_manager_example.ts to see how to use the IndexManager. Then run:

$ fss generate:index <input-path-to-js> <output-path-to-json>

The fss script gets added as a script to your node_modules

Custom error

The query functions get and getById will throw an error if the document doesn't exist. If you want to throw an custom error you can do that by passing an error factory.

export class HttpError extends Error {
  constructor(msg: string, public code: number) {
    super(msg)
  }
}

const errorFactory = (msg) => {
  return new HttpError(msg, 404);
};

Using Inversify

FirestoreStorageModule.createWithFirestore(admin.firestore(), errorFactory)

Using vanilla Typescript

class RestaurantRepository extends BaseRepository<Restaurant> {

  constructor() {
    super(storage, errorFactory);
  }
}
Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].