firestore-data-manager

Interface for creating models, schemas and validate data for Google Cloud Firestore.


Keywords
schema, model, validation, google, cloud, firestore, odm, orm
License
MIT
Install
npm install firestore-data-manager@0.1.0

Documentation

Firestore Schema Validator

Elegant object modeling for Google Cloud Firestore.

Inspired by mongoose and datalize.

Installation

Requires firebase-admine package.

npm install --save firestore-schema-validator

API Docs

DOCS.md

Usage

Schema & Model - Simple example

// UserModel.js
const { Model, schema, field } = require('firestore-schema-validator')

const userSchema = schema({
  firstName: field('First Name')
    .string()
    .trim(),
  lastName: field('Last Name')
    .string()
    .trim(),
  email: field('Email Address')
    .string()
    .email(),
})

class UserModel extends Model {
  // Path to Cloud Firestore collection.
  static get _collectionPath() {
    return 'users'
  }

  // Model Schema.
  static get _schema() {
    return userSchema
  }
}

Schema & Model - Robust example

// UserModel.js
const { Model, schema, field } = require('firestore-schema-validator')

const userSchema = schema({
  firstName: field('First Name')
    .string()
    .trim(),
  lastName: field('Last Name')
    .string()
    .trim(),
  password: field('Password')
    .string()
    .match(/[A-Z]/, '%s must contain an uppercase letter.')
    .match(/[a-z]/, '%s must contain a lowercase letter.')
    .match(/[0-9]/, '%s must contain a digit.')
    .minLength(8),
  email: field('Email Address')
    .string()
    .email(),
  emailVerificationCode: field('Email Verification Code')
    .string()
    .nullable(),
  birthDate: field('Birth Date')
    .date('YYYY-MM-DD')
    .before(
      moment()
        .subtract(13, 'years')
        .toISOString(),
      'You must be at least 13 years old.',
    ),
  options: field('Options')
    .objectOf({
      lang: field('Language')
        .oneOf([
          'en-US',
          'pl-PL'
        ])
        .default('en-US'),
    })
})

class UserModel extends Model {
  static get _collectionPath() {
    return 'users'
  }

  static get _schema() {
    return userSchema
  }

  // You can define additional methods...
  static async getByEmail(email) {
    return await this.getBy('email', email)
  }

  // ... or getters.
  get isEmailVerified() {
    return Boolean(this._data.emailVerificationCode)
  }

  get fullName() {
    return `${this._data.firstName} ${this._data.lastName}`
  }

  // this.toJSON() by default returns this._data,
  // but you might want to display it differently
  // (eg. don't show password in responses,
  // combine firstName and lastName into fullName, etc.)
  toJSON() {
    return {
      id: this._id, // ID of Document stored in Cloud Firestore
      createdAt: this._createdAt, // ISO String format date of Document's creation.
      updatedAt: this._updatedAt, // ISO String format date of Document's last update.
      fullName: this.fullName,
      email: this.email,
      isEmailVerified: this.isEmailVerified,
    }
  }
}

// Fired when new user is successfully created and stored.
UserModel.on('created', async (user) => {
  // eg. send Welcome Email to User
})

// Fired when user is successfully updated and stored.
UserModel.on('updated', async (user) => {
  // eg. log info to console
})

// Fired when user is succsessfully deleted.
UserModel.on('deleted', async (user) => {
  // eg. delete photos uploaded by User
})

// Fired during user.validate() if user.email has changed,
// but *before* actually validating and storing the data.
UserModel.prehook('email', (data, user) => {
  // eg. set emailVerificationCode
})

// Fired during user.validate() if user.email has changed,
// but *after* actually validating and storing the data.
UserModel.posthook('email', (data, user) => {
  // eg. send Email Verification Email to User
})

UserModel.posthook('password', (data, user) => {
  // eg. hash password to store it securely
})

Working with UserModel

const admin = require('firebase-admin')
const User = require('../UserModel.js')

// Initialize Firebase
admin.initailizeApp({
  // config
})

const user = await User.create({
  firstName: 'Jon',
  lastName: 'Doe',
  email: 'jon.doe@example.com',
  password: 'J0nD03!@#',
  birthDate: '1990-01-10',
}) // => instance of UserModel

console.log(user.toJSON()) // =>
// {
//   id: 'x22sSpmaJek0CYS9KTsI'.
//   createdAt: '2019-06-14T15:46:55.108Z',
//   updatedAt: null,
//   fullName: 'Jon Doe',
//   email: 'jon.doe@example.com',
//   isEmailVerified: false,
// }

user.firstName = 'J'
user.foo = 'bar' // Won't be stored as it's not defined in UserModel._schema
await user.save() // => instance of UserModel

console.log(user.toJSON()) // =>
// {
//   id: 'x22sSpmaJek0CYS9KTsI'.
//   createdAt: '2019-06-14T15:46:55.108Z',
//   updatedAt: null,
//   fullName: 'J Doe',
//   email: 'jon.doe@example.com',
//   isEmailVerified: false,
// }

const fetchedUser = await UserModel.getByEmail('jon.doe@example.com') // => instance of UserModel

console.log(fetchedUser.toJSON()) // =>
// {
//   id: 'x22sSpmaJek0CYS9KTsI'.
//   createdAt: '2019-06-14T15:46:55.108Z',
//   updatedAt: null,
//   fullName: 'J Doe',
//   email: 'jon.doe@example.com',
//   isEmailVerified: false,
// }

await fetchedUser.delete()

const nonExistingUser = await UserModel.getByEmail('jon.doe@example.com') // => null

Nesting Schema

field.arrayOf(fieldOrSchema) and field.objectOf(objectOfFieldsOrSchema) accept instances of Schema as an argument, so you can reuse repeatable schemas:

const { schema, field } = require('firestore-schema-validator')

const simplifiedAddressSchema = schema({
  street: field('Street')
    .string()
    .trim(),
  countryCode: field('Country')
    .oneOf([
      'US',
      'CA',
    ]),
  zipCode: field('ZIP Code')
    .string()
    .trim(),
})

const userSchema = schema({
  firstName: field('First Name')
    .string()
    .trim(),
  lastName: field('Last Name')
    .string()
    .trim(),
  mailingAddress: field('Mailing Address')
    .objectOf(simplifiedAddressSchema),
})

const companySchema = schema({
  name: field('Company Name')
    .string()
    .trim(),
  locations: field('Locations')
    .arrayOf(simplifiedAddressSchema),
})

TODO

  • Field.prototype.unique() that checks if the value provided to field is unique in the collection.