fiery-data
TypeScript icon, indicating that this package has built-in type declarations

1.0.0 • Public • Published

Fiery Data

fiery-data

A library which binds Firestore data to plain arrays and objects and keeps them in sync.

Features

  • Documents example
  • Collections (stored as array or map) example
  • Queries (stored as array or map) example
  • Streams (stored as array or map) example
  • Pagination example
  • Real-time or once example
  • Adding, updating, sync, removing, remove field example
  • Sub-collections (with cascading deletions!) example
  • Return instances of a class example
  • Add active record methods (sync, update, remove, clear, getChanges) example
  • Control over what properties are sent on save example
  • Encode & decode properties example
  • Timestamp/Date properties example
  • Adding the key and exists to the document example
  • Sharing, extending, defining, and global options example
  • Callbacks (error, success, missing, remove) example
  • Events (update, remove, create, destroy, missing) example
  • Custom binding / unbinding example

Related

Contents

Dependencies

  • Firebase ^5.0.0

Installation

npm

Installation via npm : npm install fiery-data --save

Examples

JS
// Firebase
var app = firebase.initializeApp({ /* firebase options */ })
var fs = firebase.firestore(app)
 
// FieryData is available through UMD
// factory for creating and destroying live data
var $fiery = FieryData()
 
// get a single document
var specificTask = $fiery(fs.doc('tasks/1'))
 
// get a live array of documents
var tasks = $fiery(fs.collection('tasks'))
 
// get a live map of documents
var taskMap = $fiery(fs.collection('tasks'), {map: true})
 
// get the current array of documents, don't update anything
var tasksOnce = $fiery(fs.collection('tasks'), {once: true})
 
// update
specificTask.name = 'New name'
$fiery.update(specificTask)
 
// get new (is not saved)
var taskUnsaved = $fiery.build(tasks, { // option initial
  name: 'New task'
})
 
// get new (saved - updates tasks once "saved")
var taskSaved = $fiery.create(tasks, {
  name: 'New saved task'
})
 
// remove
$fiery.remove(specificTask)
 
// manually stop live data
$fiery.free(tasks)
 
// no more live data, saving, or deleting. release cached values
$fiery.destroy()

Each object will contain a .uid property. This helps identify what firestore database the document is stored in, the collection, and with which options.

{
  ".uid": "1///tasks/1",
  "name": "Star fiery-date",
  "done": true
}
TypeScript

A more advanced example with classes, active record, querying, and definitions

import $getFiery, { define, FieryRecordSave, FieryRecordRemove } from 'fiery-data'
 
// classes are not required, but supported
class Task {
  name: string = ''
  done: boolean = false
  done_at: number | null = null
  done_by: string | null = null
  edited_at: number = 0
 
  finish (): void {
    this.done = true
    this.done_at = Date.now()
    this.done_by = 'Me'
    this.edited_at = Date.now()
    this.save()
  }
 
  // these are injected by recordOptions
  save: FieryRecordSave
  remove: FieryRecordRemove
}
 
// you can optional define options globally
define({
  task: {
    type: Task,
    include: ['name', 'done', 'done_at', 'done_by', 'edited_at'],
    query: q => q.orderBy('edited_at', 'desc'),
    record: true,
    recordOptions: {
      save: 'save',
      remove: 'remove'
    }
  }
})
 
// firebase
const app = firebase.initializeApp({ /* firebase options */ })
const fs = firebase.firestore(app)
 
const $fiery = $getFiery(/* options for binding to other frameworks */)
 
// a single document, kept up to date
const specificTask: Task = $fiery(fs.doc('tasks/1'), 'task')
 
// all documents in the collection, live (ordered by most recently edited)
const allTasks: Task[] = $fiery(fs.collection('tasks'), 'task')
 
// all done tasks, ordered by most recently done
const doneTasks: Task[] = $fiery(fs.collection('tasks'), {
  extends: 'task',
  query: q => q.where('done', '==', true).orderBy('done_at', 'desc')
})
 
// finish this task - which updates all the references
specificTask.finish()
 
// no more live data, saving, or deleting. release cached values
$fiery.destroy()

Another advanced example with sub collections (blog with live comments)

import $getFiery, { define, setGlobalOptions,
  FieryRecordSave, FieryRecordRemove, FieryRecordCreate, FieryRecordBuild } from 'fiery-data'
 
// Classes
class ActiveRecord {
  save: FieryRecordSave
  remove: FieryRecordRemove
  create: FieryRecordCreate
  build: FieryRecordBuild
}
 
class BlogPost extends ActiveRecord {
  title: string = ''
  content: string = ''
  author: string = ''
  url: string = ''
  tags: string[] = []
  created_at: Date
  comments: BlogPostComment[] = []
}
 
class BlogPostComment extends ActiveRecord {
  title: string = ''
  author: string = ''
  created_at: Date
  comments: BlogPostComment[] = []
}
 
// Options
setGlobalOptions({
  record: true,
  recordOptions: {
    save: 'save',
    remove: 'remove',
    create: 'create',
    build: 'build'
  }
})
 
define({
  postListing: {
    type: BlogPost,
    once: true, // we don't need to have live post data
    include: ['title', 'content', 'author', 'tags', 'url', 'created_at']
  },
  postView: {
    extends: 'postListing'
    sub{
      comments: 'comment'
    }
  },
  comment: {
    type: BlogPostComment,
    include: ['title', 'author', 'created_at'],
    sub: {
      comments: 'comment'
    }
  }
})
 
// Firestore & Fiery
const app = firebase.initializeApp({ /* firebase options */ })
const fs = firebase.firestore(app)
const $fiery = $getFiery()
 
// Functions
function getFrontPage (limit: number = 10): BlogPost[]
{
  const options = {
    extends: 'postListing',
    query: q => q.orderBy('created_at', 'desc').limit(limit)
  }
 
  return $fiery(fs.collection('posts'), options)
}
 
function getPost (id: string): BlogPost
{
  return $fiery(fs.collection('posts').doc(id), 'postView')
}
 
function getPostsByTag (tag: string, limit: number = 10): BlogPost
{
  const options = {
    extends: 'postListing',
    query: q => q
      .where('tags', 'array_contains', tag)
      .orderBy('created_at', 'desc')
      .limit(limit)
  }
 
  return $fiery(fs.collection('posts'), options, 'byTag')
}
 
function addComment (addTo: BlogPost | BlogPostComment, comment: string): BlogPostComment
{
  return addTo.create('comments', {
    title: comment,
    created_at: new Date(),
    author_id: 'CURRENT_USER'
  })
}
 
$fiery.destroy()

API

  • $fiery ( source, options?, name? )
    • source
      • fs.doc ('path/to/doc')
      • fs.collection ('items')
    • options
    • name
      • necessary when you call $fiery multiple times (like as a result of a function with parameters) or if you want to call create or build passing a string
  • $fiery.update ( data, fields? ): Promise<void>
    • data
      • the data of a document to update
    • fields
      • optionally you can pass a field name or array of fields to update (as opposed to all)
  • $fiery.save ( data, fields? ): Promise<void>
    • data
      • the data of a document to save (update if it exists, set if it does not)
    • fields
      • optionally you can pass a field name or array of fields to update (as opposed to all)
  • $fiery.sync ( data, fields? ): Promise<void>
    • data
      • the data of a document to update. any fields not on the document or specified in fields will be removed
    • fields
      • optionally you can pass a field name or array of fields to sync. any other fields in the document not specified here are removed
  • $fiery.remove ( data, excludeSubs? ): Promise<void>
    • data
      • the data of the document to remove. by default the sub collections specified in the options are removed as well
    • excludeSubs
      • if you wish, you could only remove the document data and not the sub collections
  • $fiery.clear ( data, fields ): Promise<void>
    • data
      • the data of the document to clear values of
    • fields
      • the fields to remove from the document - or sub collections to remove (if specified in the options)
  • $fiery.getChanges ( fields?, isEqual? ): Promise<{changed, remote, local}>
    • fields
      • optionally you can check specific fields for changes, otherwise all are checked
    • isEqual
      • you can pass your own function which checks two values for equality
    • returns
      • the promise resolves with an object with changed, remote, and local
        • changed is either true or false
        • remote are the changed saved values
        • local are the changed unsaved values
  • $fiery.pager ( target ): FieryPager
    • target
      • the collection to paginate
  • $fiery.ref ( data, sub? ): DocumentReference | CollectionReference
    • data
      • the data to get the firebase reference of
    • sub
      • a sub collection of the given data to return
  • $fiery.create ( target, initial? )
    • target
      • the collection to add a value to and save
    • initial
      • the initial values of the data being created
  • $fiery.createSub ( target, sub, initial? )
    • target
      • the target which has the sub collection
    • sub
      • the sub collection to add a value to and save
    • initial
      • the initial values of the data being created
  • $fiery.build ( target, initial? )
    • target
      • the collection to add a value (unsaved)
    • initial
      • the initial values of the data being built
  • $fiery.buildSub ( target, sub, initial? )
    • target
      • the target which has the sub collection
    • sub
      • the sub collection to add a value (unsaved)
    • initial
      • the initial values of the data being created
  • $fiery.free ( target ): void
    • stops live data on the target and removes cached values when possible
  • $fiery.destroy (): void
    • calls free on all targets generated with $fiery (...)

Feature Examples

Documents

// real-time documents
var settings = $fiery(fs.collection('settings').doc('system'))
 
var currentUser = $fiery(fs.collection('users').doc(USER_ID))

Collections

// real-time array
var cars = $fiery(fs.collection('cars'))
 
// real-time map: carMap[id] = car
var carMap = $fiery(fs.collection('cars'), {map: true})

Queries

// real-time array
var currentCars = $fiery(fs.collection('cars'), {
  query: cars => cars.where('make', '==', 'Honda')
})
 
// a parameterized query that can be invoked any number of times
function searchCars(make)
{
   var options = {
      query: cars => cars.where(make, '==', make)
   }
   return $fiery(fs.collection('cars'), options, 'searchCars') // name (searchCars) is required when parameterized
}
 
var cars1 = searchCars('Honda')
var cars2 = searchCars('Ford')
 
// cars1 === cars2, same array. Using the name ensures one query is no longer listened to - and only the most recent one

Streams

A stream is an ordered collection of documents where the first N are fetched, and any newly created/updated documents that should be placed in the collection are added. You can look back further in the stream using more. A use case for streams are a message channel. When the stream is first loaded N documents are read. As new messages are created they are added to the beginning of the collection. If the user wishes to see older messages they simply have to call more on the stream to load M more. The once property does not work on streams, they are real-time only.

You MUST have an orderBy clause on the query option and stream must be true.

// streams are always real-time, but can be an array or map
var messages = $fiery(
  fs.collection('messages'), {
  query: q => q.orderBy('created_at', 'desc'),
  stream: true,
  streamInitial: 25, // initial number of documents to load
  streamMore: 10 // documents to load when more is called without a count
})
 
// 25 are loaded (if that many exist)
 
// load 10 more
$fiery.more(messages)
 
// load 20 more
$fiery.more(messages, 20)

Pagination

function searchCars(make, limit)
{
   var options = {
      query: cars => cars.where('make', '==', make).orderBy('created_at').limit(limit),
      // required for prev() - orderBys must be in reverse
      queryReverse: cars => cars.where('make', '==', make).orderBy('created_at', 'desc').limit(limit)
   }
 
   // name (searchCars) is required when parameterized
   return $fiery(fs.collection('cars'), options, 'searchCars')
}
 
var cars = searchCars('Honda', 10) // 10 at a time
var pager = $fiery.pager(cars)
 
pager.next() // next 10 please, returns a promise which resolves when they're fetched
 
// pager.index // which page we're on
// pager.hasNext() // typically returns true since we don't really know - unless cars is empty
// pager.next() // executes the query again but on the next 10 results. index++
// pager.hasPrev() // looks at pager.index to determines if there's a previous page
// pager.prev() // executes the query again but on the previous 10 results. index--

Real-time or once

// real-time is default, all you need to do is specify once: true to disable it
 
// array populated once
var cars = $fiery(fs.collection('cars'), {once: true})
 
// current user populated once
var currentUser = $fiery(fs.collection('users').doc(USER_ID), {once: true}),

Adding, updating, overwriting, removing

var currentUser = $fiery(fs.collection('users').doc(USER_ID), {}, 'currentUser')
var todos = $fiery(fs.collection('todos'), {}, 'todos') // name required to get access to sources
 
function addTodo() // COLLECTIONS STORED IN stores
{
  $fiery.sources.todos.add({
    name: 'Like fiery-data',
    done: true
  })
  // OR
  var savedTodo = $fiery.create(todos, { // you can pass this.todos or 'todos'
    name: 'Love fiery-data',
    done: false
  })
}
 
function updateUser()
{
  $fiery.update(currentUser)
}
function updateUserEmailOnly()
{
 $fiery.update(currentUser, ['email'])
}
function updateAny(data) // any document can be passed, ex: this.todos[1], this.currentUser
{
  $fiery.update(data)
}
function overwrite(data) // only fields present on data will exist on sync
{
  $fiery.sync(data)
}
function remove(data)
{
  $fiery.remove(data) // removes sub collections as well
  $fiery.remove(data, true) // preserves sub collections
}
function removeName(todo)
{
  $fiery.clear(todo, 'name') // can also specify an array of props/sub collections
}

Sub-collections

You can pass the same options to sub, nesting as deep as you want!

var todos = $fiery(fs.collection('todos'), {
  sub: {
    children: { // creates an array or map on each todo object: todo.children[]
      // once, map, etc
      query: children => children.orderBy('updated_at')
    }
  }
})
 
// todos[todoIndex].children[childIndex]
 
function addChild(parent)
{
  $fiery.ref(parent).collection('children').add( { /* values */ } )
  // OR
  $fiery.ref(parent, 'children').add( { /* values */ } )
  // OR
  var savedChild = $fiery.createSub(parent, 'children', { /* values */ } )
  // OR
  var unsavedChild = $fiery.buildSub(parent, 'children', { /* values */ } )
}
 
function clearChildren(parent)
{
  $fiery.clear(parent, 'children') // clear the sub collection of all children currently in parent.children
}

Return instances of a class

function Todo() {}
Todo.prototype = {
  markDone (byUser) {
    this.done = true
    this.updated_at = Date.now()
    this.updated_by = byUser.id
  }
}
 
var todos $fiery(fs.collection('todos'), {
  type: Todo,
  // OR you can specify newDocument and do custom loading (useful for polymorphic data)
  newDocument: function(initialData) {
    var instance = new Todo()
    instance.callSomeMethod()
    return instance
  }
})

Active Record

// can be used with type, doesn't have to be
function Todo() {}
Todo.prototype = {
  markDone (byUser) {
    this.done = true
    this.updated_at = Date.now()
    this.updated_by = byUser.id
    this.$save() // injected
  }
}
 
var todos = $fiery(fs.collection('todos'), {
  type: Todo,
  record: true
  // $sync, $update, $remove, $ref, $clear, $getChanges, $build, $create, $save, $refresh are functions added to every Todo instance
})
 
todos[i].$update()
todos[i].markDone(currentUser)
todos[i].$getChanges(['name', 'done']).then((changes) => {
  // changes.changed, changes.remote, changes.local
})
 
var todosCustom = $fiery(fs.collection('todos'), {
  record: true,
  recordOptions: { // which methods do you want added to every object, and with what method names?
    save: 'save',
    remove: 'destroy'
    // we don't want $ref, $clear, $getChanges, etc
  }
})
 
todosCustom[i].save()
todosCustom[i].destroy()

Save fields

var todos = $fiery(fs.collection('todos'), {
  include: ['name', 'done'], // if specified, we ONLY send these fields on sync/update
  exclude: ['hidden'] // if specified here, will not be sent on sync/update
})
 
var todo = todos[i]
 
$fiery.update(todo) // sends name and done as configured above
$fiery.update(todo, ['done']) // only send this value if it exists
$fiery.update(todo, ['hidden']) // ignores exclude and include when specified
 
// $fiery.save also takes fields, when you're not sure if your document exists.

Encode & decode properties

var todos = $fiery(fs.collection('todos'), {
  // convert server values to local values
  decoders: {
    status(remoteValue, remoteData) {
      return remoteValue === 1 ? 'done' : (remoteValue === 2 ? 'started' : 'not started')
    }
  },
  // convert local values to server values
  encoders: {
    status(localValue, localData) {
      return localValue === 'done' ? 1 : (localeValue === 'started' ? 2 : 0)
    }
  },
  // optionally instead of individual decoders you can specify a function
  decode(remoteData) {
    // do some decoding, maybe do something special
    return remoteData
  }
})

Timestamp/Date properties

var todos = $fiery(fs.collection('todos'), {
  // automatically converts unix timestamp, Date, or Timestamp into Date instance
  timestamps: ['updated_at', 'created_at']
})

Adding key and exists to object

var todos = $fiery(fs.collection('todos'), { key: 'id', propExists: 'exists', exclude: ['id', 'exists']})
// must be excluded manually from saving if include is not specified
 
// todos[i].id      => a string identifier of the document
// todos[i].exists  => true or false if the document exists or not

Sharing, extending, defining, and global options

import { define, setGlobalOptions } from 'fiery-data'
 
// ==== Sharing ====
let Todo = {
  shared: true, // necessary for non-global or defined options that are used multiple times
  include: ['name', 'done', 'done_at']
}
 
// ==== Extending ====
let TodoWithChildren = {
  shared: true
  extends: Todo,
  sub: {
    children: Todo
  }
}
 
// ==== Defining ====
define('post', {
  // shared is not necessary here
  include: ['title', 'content', 'tags']
})
 
// or multiple
define({
  comment: {
    include: ['author', 'content', 'posted_at', 'status'],
    sub: {
      replies: 'comment' // we can reference options by name now, even circularly
    }
  },
  images: {
    include: ['url', 'tags', 'updated_at', 'title']
  }
})
 
// ==== Global ====
setGlobalOptions({
  // lets make everything active record
  record: true,
  recordOptions: {
    update: 'save',         // object.save(fields?)
    sync: 'sync',           // object.sync(fields?)
    remove: 'remove',       // object.remove()
    clear: 'clear',         // object.clear(fields)
    create: 'create',       // object.create(sub, initial?)
    build: 'build',         // object.build(sub, initial?)
    ref: 'doc',             // object.doc().collection('subcollection')
    getChanges: 'changes'   // object.changes((changes, remote, local) => {})
  }
})
 
var comments = $fiery(fs.collection('comment'), 'comment') // you can pass a named or Shared

Callbacks

var todos = $fiery(fs.collection('todos'), {
  onSuccess: (todos) => {}, // everytime todos updates this is called
  onError: (reason) => {}, // there was an error getting collection or document
  onRemove: () => {}, // document was removed
  onMissing: () => {} // document does not exist yet
})

Events

function Task() {}
Task.prototype = {
  $onUpdate: function() {
    // I've been updated
  },
  $onRemove: function() {
    // I've been removed from the firestore
  },
  $onCreate: function() {
    // This instance has been created (runs after constructor if one is given)
  },
  $onDestroy: function() {
    // This instance is being recycled. It may still exist in the firestore, but
    // it no longer is referenced by the app
  },
  $onMissing: function() {
    // This document was attempted to retrieved, but it doesn't exist yet
  }
}
 
var tasks = $fiery(fs.collection('tasks'), {
  type: Task,
  events: true
})

Or you can specify the names of the functions:

var tasks = $fiery(fs.collection('tasks'), {
  type: Task,
  events: true,
  eventsOptions: {
    update: 'onUpdate',
    remove: 'onRemove',
    create: 'init',
    destroy: 'destroy',
    missing: 'oops'
  }
})

Binding and Unbinding

var todos = $fiery(fs.collection('todos')) // will be live updated
 
$fiery.free(todos) // live updates stop

LICENSE

MIT

Versions

Current Tags

  • Version
    Downloads (Last 7 Days)
    • Tag
  • 1.0.0
    60
    • latest

Version History

Package Sidebar

Install

npm i fiery-data

Weekly Downloads

60

Version

1.0.0

License

MIT

Unpacked Size

327 kB

Total Files

58

Last publish

Collaborators

  • philip.diffenderfer