Firestorm: An ORM for Firestore

Ensure type safety and write cleaner code with Firestorm

Firestore logo

Firestorm is a Firestore ORM for TypeScript. If you’ve never used Firestore, it’s a NoSQL database that allows you to get your project off the ground, without having to worry about scalability.

If you’re familiar with SOLID principles, you’ll know that each module/class should have a single responsibility. When writing code with the native Firestore library, it’s easy to mix up code that accesses the database within your business logic. Furthermore, the data you receive will be a JSON format, and if you’re using TypeScript, it’s likely you enjoy the benefits of type safety.

Firestorm allows you to abstract away from the database layer with ease by defining your collections (and subcollections) as models, which you can then use to perform create, read, update, and delete (CRUD) operations. This helps to ensure type safety and helps write cleaner code.

Requirements

Firestorm relies on using TypeScript’s experimental decorators for defining your models. Please ensure you have the following in your tsconfig.json ( ES5 is the minimum target):

{

"compilerOptions": {

"target": "ES5",

"experimentalDecorators": true,

"emitDecoratorMetadata": true,

}

}

Installation

$ npm install firebase-firestorm

Getting Started

In this section, we will walk you through an example of how a basic blogging database might look using posts, comments, and authors.

1. Initialize Firestorm

Call firestorm.initialize(firestore, options?) as soon as you initialize your Firestore app.

import * as firestorm from 'firebase-firestorm';

...

const firestore = firebase.initializeApp(...);

firestorm.initialize(firestore, /* options */);

...

2. Define root collections

Here we have a class representing a posts collection. Entity classes are typically nonpluralized as they represent a single document from that collection. To define a root collection, you must:

Extend from the Entity class.

class. Annotate your class with @rootCollection(opts: ICollectionConfig) .

. Declare a series of fields, annotated with @field(opts: IFieldConfig) .

import { Entity, rootCollection, field } from 'firebase-firestorm'; @rootCollection({

name: 'posts',

})

export default class Post extends Entity {

@field({ name: 'title' })

title!: string; @field({ name: 'content' })

content!: string;

}

3. Defining subcollections

Each of your models, whether they represent a root collection or subcollection must extend from the Entity class provided.

Now we want documents in the posts collection to have a subcollection of comments . First, we need to create a class for the comments. Notice how we do not annotate the class with @rootCollection .

import { Entity, rootCollection, field } from 'firebase-firestorm'; export default class Comment extends Entity {

@field({ name: 'content' })

content!: string; @field({ name: 'by' })

by!: string;

}

Back in the Post class, we can add Comment as a subcollection using the @subCollection(opts: ISubcollectionConfig) decorator.

import { Entity, ICollection, rootCollection, field } from 'firebase-firestorm';

import Comment from './Comment'; @rootCollection({

name: 'posts',

})

export default class Post extends Entity {

...

@subCollection({

name: 'comments',

entity: Comment, // we must define the entity class due to limitations in Typescript's reflection capabilities. Progress should be made on this issue in future releases.

})

comments!: ICollection<Comment>;

...

}

4. Defining document references

Finally, we want documents in the posts collection to reference an author in an authors collection (another root collection). First, we define the Author entity:

import { Entity, rootCollection, field } from 'firebase-firestorm'; @rootCollection({

name: 'authors',

})

export default class Author extends Entity {

@field({ name: 'name' })

name!: string;

}

Then we can add an Author reference to the Post entity using the @documentRef(opts: IDocumentRefConfig) decorator:

import { Entity, ICollection, IDocumentRef, rootCollection, field } from 'firebase-firestorm';

import Author from './Author'; @rootCollection({

name: 'posts',

})

export default class Post extends Entity {

...

@documentRef({

name: 'author',

entity: Author, // we must define the entity class due to limitations in Typescript's reflection capabilities. Progress should be made on this issue in future releases.

})

author!: IDocumentRef<Author>;

...

}

5. Querying/updating data

Now that we’ve built our model, we’re ready to start querying. Calling Collection(entity : IEntity) will return a list of methods users can use to manipulate the data.

Getting a document

...

const post = Collection(Post).get('post-1').then((post : Post) => {

console.log(post);

});

...

Getting a subcollection

In our example, Comment is a subcollection of Post . You can get subcollections from a retrieved document or a document reference.

// Comment subcollection from document.

const post = Collection(Post).get('post-1').then((post : Post) => {

const commentCollection = post.collection(Post);

}); // Comment subcollection from document ref.

const postRef = Collection(Post).doc('post-1');

const commentCollection = postRef.collection(Post);

Querying data

You can use the find(query : ICollectionQuery) method to query data. A full list of options are available in the docs, but they are essentially the same as what is available with Firestore.

const posts = Collection(Post).find({

where: [

['title', '==', 'Example Title'],

...

],

});

Creating documents

...

const post = new Post();

post.id = 'post-1'; // id is optional, if it is not defined it will be generated by firestore.

post.title = 'Untitled';

let savedPost : Post;

Collection(Post).create(post).then((_savedPost : Post) => {

savedPost = _savedPost;

});

...

Updating documents

...

const post = new Post();

post.id = 'post-1'; // id is required.

post.title = 'Untitled';

let savedPost : Post;

Collection(Post).update(post).then((_savedPost : Post) => {

savedPost = _savedPost;

});

...

Removing documents

...

Collection(Post).remove('post-id').then(...);

...

6. Formatting data

An instance of the entity may contain properties such as subcollections which you do not wish to include if, for example, you are building a REST API. Calling the toData() method on an instance of an entity will produce a plain JSON object containing just primitive data, nested JSON objects, and document references which have already been retrieved using the .get() method. For example:

import { Collection } from 'firebase-firestorm';

import Author from './Author';

import Post from './Post'; Collection(Post).get('post-1').then((post: Post) => {

console.log(post.toData());

/*

Output:

{

id: ...,

title: ...,

content: ...

}

*/

post.author.get().then((author: Author) => {

console.log(post.toData());

/*

Output:

{

id: ...,

title: ...,

content: ...,

author: {

id: ...,

name: ...

}

}

*/

});

});

Custom Data Types

Arrays

Firestore documents can contain arrays of strings, numbers, objects, etc. Defining arrays in Firestorm is as simple as assigning properties as array types in your Entity files. For example:

class Example extends Entity {

@field({ name: 'example_property_1' })

property1!: string[]; @field({ name: 'example_property_2' })

property2!: IDocumentRef<AnotherEntity>[];

}

Nested data

Firestore documents can contain nested objects (or maps). For a nested object, you need to create a new class to represent that object and add a property with that class in your Entity , wrapped with the @map decorator.

class Example extends Entity {

@map({ name: 'nested_object' })

nestedObject!: Nested;

} class Nested {

@field({ name: 'nested_property' })

nestedProperty!: string;

}

And then use this entity:

...

const nested = new Nested();

nested.nestedProperty = 'test';

const example = new Example();

example.nestedObject = nested;

...

Geopoints

Geopoints store locational data and can be used a field. We have a wrapper class for Firestore’s GeoPoint which basically serves the same functionality.

class Example extends Entity {

@geopoint({

name: 'geopoint_property',

})

geopoint!: IGeoPoint;

}

And then assign a GeoPoint:

...

const example = new Example();

example.geopoint = new Geopoint(latitude, longitude);

...

Timestamps

You can represent date and time data in your Entity files. Like GeoPoints, our timestamp representation is essentially a wrapper of Firestore's. You can set the options for the field to updateOnWrite which uses the server timestamp when creating or updating documents, or use updateOnCreate or updateOnUpdate .

class Example extends Entity {

@timestamp({

name: 'timestamp_property',

updateOnWrite: true,

})

timestamp!: ITimestamp;

}

Important Gotchas

All files for root collections, subcollections, and nested maps must have a unique class name due to the way the metadata storage hooks everything up. We’re looking for a way to resolve this issue.

Make sure fields such as GeoPoints, timestamps, and document references have the I infront of the type, e.g. IDocumentRef , ITimestamp , IGeoPoint .

Limitations

Listening to document updates using snapshots is currently unsupported

Transactions and batched writes are currently unsupported

Offline persitences is unsupported

Wrapping Up

That should be enough to get you going. For more information, check out the repo on Github, or check out the docs.