Nashville Plays Music

    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

    Install

    npm i fiery-data

    DownloadsWeekly Downloads

    184

    Version

    1.0.0

    License

    MIT

    Unpacked Size

    327 kB

    Total Files

    58

    Last publish

    Collaborators

    • philip.diffenderfer