fiery-data
v1.0.0
Published
A Typescript/JS library for interacting with Google Firebase Database and Firestore
Downloads
259
Maintainers
Readme
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
- fiery-vue: fiery-data for VueJS
- fiery-vuex: fiery-data for Vuex
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 of options passed to
define
- checkout this file to see the available values
- name of options passed to
- name
- necessary when you call
$fiery
multiple times (like as a result of a function with parameters) or if you want to callcreate
orbuild
passing astring
- necessary when you call
- source
$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)
- data
$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)
- data
$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
- data
$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
- data
$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)
- data
$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
, andlocal
changed
is either true or falseremote
are the changed saved valueslocal
are the changed unsaved values
- the promise resolves with an object with
- fields
$fiery.pager ( target ): FieryPager
- target
- the collection to paginate
- target
$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
- data
$fiery.create ( target, initial? )
- target
- the collection to add a value to and save
- initial
- the initial values of the data being created
- target
$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
- target
$fiery.build ( target, initial? )
- target
- the collection to add a value (unsaved)
- initial
- the initial values of the data being built
- target
$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
- target
$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 (...)
- calls free on all targets generated with
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