All Projects → lmcq → Firebase Firestorm

lmcq / Firebase Firestorm

Licence: mit
Firebase Firestorm is an ORM for Firestore which can be used with Typescript.

Programming Languages

typescript
32286 projects

Firebase Firestorm for Typescript

Build Status License: MIT Codacy Badge Codacy Badge

Firestorm is an ORM for firestore which can be used with Typescript.

This library currently only supports the client Firebase SDK.

Contents

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 minimum target):

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
  }
}

Installation

$ npm install firebase-firestorm

Usage

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. See intialization options for more information about intiailizing firestorm.

import * as firestorm from 'firebase-firestorm';
...
const firestore = firebase.initializeApp(...).firestore();
firestorm.initialize(firestore, /* options */);
...

2. Defining root collections

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

  • Extend from the Entity 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 we've built our model, we're ready to start querying. Calling Collection(entity : IEntity) will return a list of methods use 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(Comment);
});

// Comment subcollection from document ref.
const postRef = Collection(Post).doc('post-1');
const commentCollection = postRef.collection(Comment);
// Finds all comments from commentCollection.
const commentsSnap = await commentCollection.find();

Querying data

Calling query() on a collection will allow you to build queries in a similar fashion to the standard Firestore SDK. You can build a query by chaining together methods, and finally calling the get() method to fetch the result. Omitting filters after the query() method will return all results from a collection.

// Build the query.
const query = Collection(Post)
  .query()
  .where('title', '==', 'Example Title');

// Fetch and manipulate the result.
query.get().then((snapshot) => {
  const post = snapshot.docs;
  ...
});
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(...);

5. Realtime Updates

You can set up listeners for changes on either a single document, or a group of documents for a query. This is done a similar way to the standard Firebase SDK.

Listening to document updates

You can attach a listener to a single document reference by using the onSnapshot(callback) method.

Collection(Post).doc('post-id').onSnapshot(
  (snapshot): DocumentSnapshot<Post> => {
    const post: Post = snapshot.doc;
  }
);
...

The callback function will executed once with the initial snapshot payload, and then for any subsequent updates to that document.

Listening to a collection (or query)

You can attach a listener to a group of documents in a collection by using the onSnapshot(callback) method on a collection query.

Collection(Post).query().onSnapshot(
  (snapshot): QuerySnapshot<Post> => {
    const posts: Post[] = snapshot.docs;
  }
);

// or

Collection(Post)
  .query()
  .where('title', '==', 'Example Title')
  .onSnapshot(
    (snapshot): QuerySnapshot<Post> => {
      const posts: Post[] = snapshot.docs;
    }
  );

The callback function will executed once with the initial snapshot payload, and then for any subsequent updates to that query. As per the Firebase SDK, you call see the document changes in each snapshot using the snapshot.docChanges() method.

6. Formatting data

An instance of entity maybe 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 reference 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 contains 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 to use this entity:

const nested = new Nested();
nested.nestedProperty = 'test';
const example = new Example();
example.nestedObject = nested;

Important: If your is nested data is an array you must provide the 'entity' option in the configuration.

class Nested {
  @map({ name: 'nested_array', entity: Nested })
  nestedObject: Nested[];
}

Geopoints

Geopoints store locational data and can be used as fields. 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 to assign a GeoPoint:

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

Timestamps

You can represent date & 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 updateOnCreate or updateOnUpdate.

class Example extends Entity {
  @timestamp({
    name: 'timestamp_property',
    updateOnWrite: true,
  })
  timestamp!: ITimestamp;
}

Initialization Options

firestorm.intialize({ ...opts : IFireormConfig }) can be called with the following options:

Option Description Type
fieldConversion Providing this option will convert Entity propertity names into firestore collection names so you don't need to provide the name option in @field() decorators. To view available values please check out the docs. enum FieldConversionType

Important Gotcha's

  • 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 currently looking for a way to resolve this issue.

  • Make sure fields such as geopoints, timestamps and document reference's have the I infront of the type, e.g. IDocumentRef, ITimestamp, IGeoPoint.

Limitations

  • Transactions and batched writes are currently unsupported.

If you would like to help resolve these issues, feel free to make a a pull request.

Development

Setup

  1. Clone the repo.
  2. Install dependencies.
cd firebase-firestorm 
npm install

Testing

The testing script looks for *.spec.ts files in the src and test directory.

npm test

Contributing

Found a bug?

Please report any bugs you have found submitting an issue to our Github repository, after ensuring the issue doesn't already exist. Alternatively, you can make a pull request with a fix.

Pull Requests

If you would like to help add or a feature or fix a bug, you can do so by making a pull request. The project uses Conventional Commits, so please make sure you follow the spec when making PRs. You must also include relevant tests.

License

MIT

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].