npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

firerecord

v1.0.6

Published

🔥 ActiveRecord-style Object-Relational Mapper for Cloud Firestore

Downloads

6

Readme

FireRecord

🔥 ActiveRecord-style Object-Relational Mapper for Cloud Firestore. Inspired by TypeORM and pring.ts.

Installation

$ yarn add firerecord

Then initialize FireRecord with the Firestore instance:

import * as admin from 'firebase-admin';
import { initialize } from 'firerecord';

admin.initializeApp();
initialize(admin.firestore());

TypeScript configuration

Enable experimentalDecorators option in tsconfig.json.

"experimentalDecorators": true

Quick Start

With FireRecord, your model looks like this:

import { Entity, Field } from 'firerecord';

class User extends Entity {

  @Field()
  public name!: string;

  @Field()
  public gender!: string;
}

And your domain logic looks like this:

await User.create({
  name: 'mu29',
  gender: 'male',
});

// Alternatively, you can use User.find(id) if you know id.
const user = await User.findBy({
  name: 'mu29',
});

if (!user) {
  return;
}

await user.update({
  gender: 'female',
});

await user.destroy();

Usage

In this section, we'll see 4 decorators in FireRecord and how to manage data.

Defining models

Every FireRecord model should extend the Entity class. FireRecord automatically detects a Firestore collection name from the model class name like ActiveRecord. (We use pluralize package)

import { Entity } from 'firerecord';

class User extends Entity {
}

Field Decorator

To add Firestore Document property, you simply need to decorate an entity's property with a @Field decorator.

import { Entity, Field } from 'firerecord';

class User extends Entity {

  @Field()
  public name!: string;

  @Field()
  public gender!: string;
}

You can set the property's default value when using the @Field decorator.

import { Entity, Field } from 'firerecord';

class User extends Entity {

  @Field()
  public name!: string;

  @Field({ default: 'male' })
  public gender!: string;
}

Embedded decorator

Since there is no JOIN query in Firestore, it's a good idea to embed data from other models unless you need strong consistency. Use @Embedded decorator to embed data from other models, and just put the property names you want to embed in the second parameter.

import { Entity, Field, Embedded } from 'firerecord';

class User extends Entity {

  @Field()
  public name!: string;

  @Field({ default: 'male' })
  public gender!: string;
}

class Article extends Entity {

  @Field()
  public title!: string;

  @Field()
  public content!: string;

  @Embedded(User, ['name'])
  public user!: User;
}

The article entity's data will looks like this:

{
  id: 'Mbfc08V59aQCY4yWnz75',
  title: 'My Great Post',
  content: 'Awesome Content',
  user: {
    id: 'OOFh7DiDuEoSGSMu1OfQ',
    name: 'mu29',
  },
}

You can use the cascade option with the @Embedded decorator. In the example above, let's say you want update an article's user information.

const article = await Article.find('Mbfc08V59aQCY4yWnz75');

if (!article) {
  return;
}

article.update({
  user: {
    name: 'some fancy name',
  },
});

This will updates the user's name embedded in the article, but does not change the original user model's name. Putting the cascade option will make it works as expected.

class Article extends Entity {

  @Field()
  public title!: string;

  @Field()
  public content!: string;

  @Embedded(User, ['name'], { cascade: true })
  public user!: User;
}

It is also possible to embed an array of entities.

import { Entity, Field, Embedded } from 'firerecord';

class User extends Entity {

  @Field()
  public name!: string;

  @Field({ default: 'male' })
  public gender!: string;
}

class Tag extends Entity {

  @Field()
  public name!: string;
}

class Article extends Entity {

  @Field()
  public title!: string;

  @Field()
  public content!: string;

  @Embedded(User, ['name'])
  public user!: User;

  @Embedded(Tag, ['name'])
  public tags!: Tag[];
}

There is a difference between updating an embedded entity list and an embedded entity, but we will see it later in update section.

Filtery` decorator

How do you deal with complex queries, such as all posts from one user sorted by creation date? Firestore docs suggested a decent solution: Encode all data about the query into a single map field. It looks like this:

{
  id: 'Mbfc08V59aQCY4yWnz75',
  title: 'My Great Post',
  content: 'Awesome Content',
  user: {
    id: 'OOFh7DiDuEoSGSMu1OfQ',
    name: 'mu29',
  },
  createdAt: 1569903714364,
  updatedAt: 1569903714364,
  filterBy: {
    user: {
      OOFh7DiDuEoSGSMu1OfQ: 1569903714364,
    }
  },
}

So we implemented it, the @FilterBy decorator.

import { Entity, Field, Embedded, FilterBy } from 'firerecord';

class Article extends Entity {

  @Field()
  public title!: string;

  @Field()
  public content!: string;

  @Embedded(User, ['name'])
  @FilterBy()
  public user!: User;

  @Embedded(Tag, ['name'])
  @FilterBy()
  public tags!: Tag[];
}

It will automatically create filterBy property with entity's createdAt value.

{
  id: 'Mbfc08V59aQCY4yWnz75',
  title: 'My Great Post',
  content: 'Awesome Content',
  user: {
    id: 'OOFh7DiDuEoSGSMu1OfQ',
    name: 'mu29',
  },
  tags: [{
    id: 'c2OTYSydklJoQBNLL9TJ',
    name: 'hashtag',
  }, {
    id: 'g4taIbpA79N5ECAcilZo',
    name: 'test',
  }],
  createdAt: 1569903714364,
  updatedAt: 1569903714364,
  filterBy: {
    user: {
      OOFh7DiDuEoSGSMu1OfQ: 1569903714364,
    },
    tags: {
      c2OTYSydklJoQBNLL9TJ: 1569903714364,
      g4taIbpA79N5ECAcilZo: 1569903714364,
    },
  },
}

Nested decorator

Firestore sub-collections can be represented by @Nested decorator.

import { Entity, Field, Embedded, FilterBy, Nested } from 'firerecord';

class Comment extends Entity {
  @Field()
  public content!: string;
}

class Article extends Entity {

  @Field()
  public title!: string;

  @Field()
  public content!: string;

  @Embedded(User, ['name'])
  @FilterBy()
  public user!: User;

  @Embedded(Tag, ['name'])
  @FilterBy()
  public tags!: Tag[];

  @Nested(Comment)
  public comments!: NestedEntityList<Comment>;
}

Create

If you've used ActiveRecord, you will be familiar with it.

const user = new User({
  name: 'mu29',
});

await user.save();

const tag1 = await Tag.create({
  name: 'hashtag',
});

const tag2 = await Tag.create({
  name: 'test',
});

const article = await Article.create({
  title: 'My Great Post',
  content: 'Awesome Content',
  user,
  tags: [tag1, tag2],
});

Note that nested entities must be created separately.

const comment = await article.comments.create({
  content: 'First Comment :)',
});

Find

Finding an Entity is also similar to ActiveRecord, but applying range filters came from TypeORM.

const user = await User.find('OOFh7DiDuEoSGSMu1OfQ');

const article = await Article.findBy({
  title: 'My Great Post',
});

There are 6 operators: Equal, LessThan, LessThanOrEqual, MoreThan, MoreThanOrEqual, and ArrayContains. Here's the use case:

import { LessThan } from 'firerecord';

const article = await Article.findBy({
  createdAt: LessThan(1570006534722),
});

Also, you can query the properties of an embedded entity.

const article = await Article.findBy({
  user: {
    name: 'mu29',
  },
});

lazy option

If you do not need to read the data, such as just updating an attributes, you can use the lazy option.

const article = await Article.find('Mbfc08V59aQCY4yWnz75', { lazy: true });

Where

There is also a where function that allows you to retrieve a list of entities. There is a condition option that we used before on find and findBy function.

import { MoreThan } from 'firerecord';

const articles = await Article.where({
  conditions: {
    createdAt: MoreThan(1566903714231),
  },
});

Filter option is supported on the property that is decorated with @FilterBy. Note that you can only use one filter.

const articles = await Article.where({
  conditions: {
    createdAt: MoreThan(1566903714231),
  },
  filter: {
    user: 'OOFh7DiDuEoSGSMu1OfQ',
  },
});

Pagination is supported by the cursor option. It will returns the entities after the cursor using Firestore's startAfter function.

const articles = await Article.where({
  conditions: {
    createdAt: MoreThan(1566903714231),
  },
  filter: {
    user: 'OOFh7DiDuEoSGSMu1OfQ',
  },
  cursor: 'Mbfc08V59aQCY4yWnz75',
});

Finally, we also support the order option, which is a bit complicated. Normally, it takes an array of strings where the first element is an property name and the second element is an direction type. (asc or desc)

const articles = await Article.where({
  conditions: {
    createdAt: MoreThan(1566903714231),
  },
  order: ['createdAt', 'desc'],
});

However, if we use the filter option, it only accepts the direction type.

const articles = await Article.where({
  conditions: {
    createdAt: MoreThan(1566903714231),
  },
  filter: {
    user: 'OOFh7DiDuEoSGSMu1OfQ',
  },
  order: 'asc',
});

Update

Since we use the merge option in Firestore when updating entity, all data (including the embedded entity) will be merged, not replaced.

await Article.update('Mbfc08V59aQCY4yWnz75', {
  content: 'Updated Content',
  user: {
    name: 'injung',
  },
});

// or

const article = await Article.find('Mbfc08V59aQCY4yWnz75', { lazy: true });

if (!article) {
  return;
}

article.update({
  content: 'Updated Content',
  user: {
    name: 'injung',
  },
});

However, an embedded entity list will be replaced, not merged.

Destroy

For now, deleting an entity does not erase the embedded data.

await User.destroy('OOFh7DiDuEoSGSMu1OfQ');

// or

const user = await User.find('OOFh7DiDuEoSGSMu1OfQ', { lazy: true });
await user.destroy();

// Caution: the user data embedded in the article entity remains.

Nested Entities

Nested Entities can also use the create, update, find, findBy, where, and destroy functions we've seen so far!

Transaction

Pass an array of Entity#save functions that you what to be processed at once as an argument to the runTransaction function.

import { runTransaction } from 'firerecord';

const user = await User.find('OOFh7DiDuEoSGSMu1OfQ');

const article = new Article({
  title: 'My Great Post',
  content: 'Awesome Content',
  user,
});

user.name = 'New fancy name';

await runTransaction([
  article.save,
  user.save,
]);

Contributing

Bug reports and pull requests are welcome on GitHub.

License

The package is available as open source under the terms of the MIT License.