https://github.com/lmcq/firebase-firestorm
Firebase Firestorm is an ORM for Firestore which can be used with Typescript.
https://github.com/lmcq/firebase-firestorm
Last synced: 8 months ago
JSON representation
Firebase Firestorm is an ORM for Firestore which can be used with Typescript.
- Host: GitHub
- URL: https://github.com/lmcq/firebase-firestorm
- Owner: lmcq
- License: mit
- Archived: true
- Created: 2019-08-08T15:49:17.000Z (over 6 years ago)
- Default Branch: master
- Last Pushed: 2022-12-09T21:50:18.000Z (about 3 years ago)
- Last Synced: 2025-04-29T06:32:09.733Z (9 months ago)
- Language: TypeScript
- Size: 575 KB
- Stars: 251
- Watchers: 7
- Forks: 17
- Open Issues: 28
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE.md
Awesome Lists containing this project
- awesome-list - firebase-firestorm
README
# Firebase Firestorm for Typescript
[](https://travis-ci.org/lmcq/firebase-firestorm)
[](https://opensource.org/licenses/MIT)
[](https://www.codacy.com/app/lmcq/firebase-firestorm?utm_source=github.com&utm_medium=referral&utm_content=lmcq/firebase-firestorm&utm_campaign=Badge_Grade)
[](https://www.codacy.com/app/lmcq/firebase-firestorm?utm_source=github.com&utm_medium=referral&utm_content=lmcq/firebase-firestorm&utm_campaign=Badge_Coverage)
Firestorm is an [ORM](https://en.wikipedia.org/wiki/Object-relational_mapping)
for [firestore](https://firebase.google.com/docs/firestore) which can be used
with Typescript.
**This library currently only supports the [client Firebase SDK](https://github.com/firebase/firebase-js-sdk).**
## Contents
- [Requirements](#requirements)
- [Installation](#installation)
- [Usage](#usage)
- [Getting Started](#getting-started)
- [Custom Data Types](#custom-data-types)
- [Initialization Options](#initialization-options)
- [Important Gotcha's](#important-gotchas)
- [Limitations](#limitations)
- [Development](#development)
- [Setup](#setup)
- [Testing](#testing)
- [Contributing](#contributing)
- [License](#license)
## Requirements
Firestorm relies on using Typescript's
[experimental decorators](https://www.typescriptlang.org/docs/handbook/decorators.html)
for defining your models. Please ensure you have the following in your `tsconfig.json`
(ES5 is minimum target):
````json
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
}
}
````
## Installation
```bash
$ 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](###-initialization-options)
for more information about intiailizing firestorm.
````typescript
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)`.
```typescript
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`.
```typescript
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.
```typescript
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;
...
}
```
#### 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:
```typescript
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:
```typescript
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;
...
}
```
#### 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
```typescript
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.
````typescript
// 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](https://firebase.google.com/docs/firestore/query-data/queries). 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.
```typescript
// 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
```typescript
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
```typescript
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
```typescript
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](https://firebase.google.com/docs/firestore/query-data/listen).
##### Listening to document updates
You can attach a listener to a single document reference by using the `onSnapshot(callback)` method.
````typescript
Collection(Post).doc('post-id').onSnapshot(
(snapshot): DocumentSnapshot => {
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.
````typescript
Collection(Post).query().onSnapshot(
(snapshot): QuerySnapshot => {
const posts: Post[] = snapshot.docs;
}
);
// or
Collection(Post)
.query()
.where('title', '==', 'Example Title')
.onSnapshot(
(snapshot): QuerySnapshot => {
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](https://firebase.google.com/docs/firestore/query-data/listen#view_changes_between_snapshots) 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:
```typescript
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:
```typescript
class Example extends Entity {
@field({ name: 'example_property_1' })
property1!: string[];
@field({ name: 'example_property_2' })
property2!: IDocumentRef[];
}
```
#### 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.
```typescript
class Example extends Entity {
@map({ name: 'nested_object' })
nestedObject!: Nested;
}
class Nested {
@field({ name: 'nested_property' })
nestedProperty!: string;
}
```
And then to use this entity:
```typescript
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.
```typescript
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.
```typescript
class Example extends Entity {
@geoPoint({
name: 'geopoint_property',
})
geopoint!: IGeoPoint;
}
```
And then to assign a GeoPoint:
```typescript
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`.
```typescript
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](#pull-requests).
## Development
### Setup
1. Clone the repo.
2. Install dependencies.
```bash
cd firebase-firestorm
npm install
```
### Testing
The testing script looks for `*.spec.ts` files in the `src`
and `test` directory.
```bash
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](https://www.conventionalcommits.org), so please
make sure you follow the spec when making PRs. You must also
include relevant tests.
## License
[MIT](license.md)