Skip to content
ReviewablePublic

About

A promise-centric Firebase library for NodeJS

Resources

Stars

10 stars

Watchers

5 watching

Forks

Repository files navigation

NodeFire

Project Status: Active - The project has reached a stable, usable state and is being actively developed.

NodeFire expands the Firebase Admin SDK with the following features:

  1. Any paths passed in are treated as templates and interpolated within an implicit or explicit scope, avoiding manual (and error-prone) string concatenation. Characters forbidden by Firebase are automatically escaped.
  2. Since one-time fetches of a reference are common in server code, a new get method makes them easy and an optional LRU cache keeps the most used ones pinned and synced to reduce latency.
  3. Transactions prefetch the current value of the reference to avoid having every transaction re-executed at least twice.
  4. A childrenKeys() method allows you to perform a shallow query to fetch a Reference's children keys without fetching all of its data.
  5. You have the option of obtaining security rule traces when an operation fails due to permission denied.

If you'd like to be able to use generators as on or once callbacks, make sure to set Promise.co to a co-compatible function.

Example

const co = require('co');
const admin = require('firebase-admin');
const NodeFire = require('nodefire');

NodeFire.setCacheSize(10);

admin.initializeApp(
  // ...
);

const db = new NodeFire(admin.database().ref())

co(
  (function*() {
    var stuff = db.child('stuffs', {
      foo: 'bar',
      baz: {
        qux: 42
      }
    });

    var data = yield {
      theFoo: stuff.child('foos/:foo').get(),
      theUser: stuff.root.child('users/{baz.qux}').get(),
    };

    yield [
      stuff.child('counters/:foo').transaction((value) => value + 1),
      stuff.child('bars/:foo/{theUser.username}', data).set(data.theFoo.bar),
    ];
  })()
);

TypeScript write placeholders

NodeFire supports separate read and write type shapes. The optional second generic parameter defines special write-value patterns, and defaults to none.

Each pattern is [keyPattern, baseType, specialValue], where specialValue is allowed for keys matching keyPattern when the field includes baseType. All nested write properties also accept null to support Firebase delete semantics.

import NodeFire from 'nodefire';

type CounterIncrement = {'.sv': {increment: number}};
type WritePatterns = [
  [`${string}timestamp`, number, {'.sv': string}],
  [`${string}Count`, number, CounterIncrement]
];

const db = new NodeFire<FirebaseData, WritePatterns>(admin.database().ref());

Typed navigation retains the database schema: child() specializes the value and write types, parent restores the immediate parent types, and root restores the database-root types. A widened runtime child path has an unknown value and parent type, while its root remains typed.

Expected permission rejections

When permission debugging is enabled, rejected operations normally wait for a serialized Firefight simulation before returning the error. All methods that invoke the permission debugger (get, on, set, update, remove, push, and transaction) accept an options object with {debugPermissionDenied: false} for expected permission rejections. This returns the original Firebase error without running that diagnostic simulation, including when a transaction's prefetch fails or a listener is cancelled. For on, the options argument follows the existing context argument. Firebase security rules, error metadata, operation interceptors, and diagnostics for operations without the opt-out are unchanged.

await ref.update(value, {timeout: 10000, debugPermissionDenied: false});
await ref.get({debugPermissionDenied: false});
ref.on('value', callback, cancelCallback, undefined, {debugPermissionDenied: false});

API

This is reproduced from the source code, which is authoritative.

/**
 * A wrapper around a Firebase Admin reference, and the main entry point to the module.  You can
 * pretty much use this class as you would use the Firebase class.  The major difference is that
 * each NodeFire object has a scope dictionary associated with it that's used to interpolate any
 * path used in a call.
 *
 * Every method that returns a promise also accepts an options object as the last argument.  One
 * standard option is `timeout`, which will cause an operation to time out after the given number of
 * milliseconds.  Other operation-specific options are described in their respective doc comments.
 */
class NodeFire;

/**
 * Creates a new NodeFire wrapper around a raw Firebase Admin reference.
 *
 * @param {admin.database.Query} refOrUrl A fully authenticated Firebase Admin reference or query.
 * @param {Object} scope Optional dictionary that will be used for interpolating paths.
 */
constructor(ref, scope)

/**
 * Flag that indicates whether to log transactions and the number of tries needed.
 * @type {boolean} True to log metadata about every transaction.
 */
static LOG_TRANSACTIONS = false

/**
 * Returns a placeholder value for auto-populating the current timestamp (time since the Unix
 * epoch, in milliseconds) as determined by the Firebase servers.
 * @return {Object} A timestamp placeholder value.
 */
static SERVER_TIMESTAMP

/**
 * Turns Firebase low-level connection logging on or off.
 * @param {boolean} enable Whether to enable or disable logging.
 */
static enableFirebaseLogging(enable)

/**
 * Turns debugging of permission denied errors on and off for the database this ref is attached
 * to.  When turned on, permission denied errors will have an additional permissionTrace property
 * with a human-readable description of which security rules failed.  There's no performance
 * penalty to turning this on until a permission actually gets denied.
 * @param {string} legacySecret A legacy database secret, needed to access the old API that allows
 *     simulating request with debug feedback.  Pass a falsy value to turn off debugging.
 */
enablePermissionDebugging(legacySecret)

/**
 * Adds an intercepting callback before or after all NodeFire database operations.
 * @param {Function} callback The callback to invoke.  It will be passed an operation descriptor
 *     ({ref, method, args}) and the operation's options object.  Before callbacks can modify the
 *     options.  After callbacks receive the same descriptor decorated with optional `startTime`,
 *     `duration`, `error`, and transaction metadata.  Transaction duration excludes prefetch;
 *     `startTime` and `duration` are omitted if no try was started.
 *     A returned promise blocks the operation from advancing past the selected trigger until it
 *     settles.  If an after callback fails after a successful operation, its error is propagated
 *     to the caller.  If both the operation and an after callback fail, the operation error is
 *     retained and the callback error is attached as its `cause` (or `interceptorError` if it
 *     already had a cause).
 * @param {'before' | 'after'} trigger The callback trigger.  Defaults to `before`.
 */
static interceptOperations(callback, trigger = 'before')

/**
 * Sets the maximum number of values to keep pinned and updated in the cache.  The cache is not used
 * unless you set a non-zero maximum.  Setting a new size clears the cache.
 * @param {number} max The maximum number of values to keep pinned in the cache.
 */
static setCacheSize(max)

/**
 * Gets the current number of values pinned in the cache.
 * @return {number} The current size of the cache.
 * @deprecated Use `getCacheStats().count` instead.
 */
static getCacheCount()

/**
 * Gets the current cache statistics.
 * @return The current cache statistics as `{count, maxSize, hits, misses, hitRate}`.  Hits include
 *     requests covered by a cached ancestor.
 */
static getCacheStats()

/**
 * Gets the current cache hit rate.
 * @return {number} The cache's current hit rate.
 * @deprecated Use `getCacheStats().hitRate` instead.
 */
static getCacheHitRate()

/**
 * Resets the cache's hit and miss counters back to zero.
 */
static resetCacheStats()

/**
 * Resets the cache's hit and miss counters back to zero.
 * @deprecated Use `resetCacheStats()` instead.
 */
static resetCacheHitRate()

/**
 * Escapes a string to make it an acceptable Firebase key.
 * @param {string} key The proposed key to escape.
 * @return {string} The escaped key.
 */
static escape(key)

/**
 * Unescapes a previously escaped (or interpolated) key.
 * @param {string} key The key to unescape.
 * @return {string} The original unescaped key.
 */
static unescape(key)

/**
 * @property {string} The path component of the reference's URL.
 */
path

/**
 * Properties which work the same as the standard Firebase Admin SDK.
 */
key
ref
root
parent
database

/**
 * Interpolates variables into a template string based on the object's scope (passed into the
 * constructor, if any) and the optional scope argument.  Characters forbidden by Firebase are
 * escaped into a "\xx" hex format.
 *
 * @param  {string} string The template string to interpolate.  You can refer to scope variables
 *     using both ":varName" and "{varName}" notations, with the latter also accepting dot-separated
 *     child attributes like "{varName.child1.child2}".
 * @param  {Object} scope Optional bindings to add to the ones carried by this NodeFire object.
 *     This scope takes precedence if a key is present in both.
 * @return {string} The interpolated string
 */
interpolate(string, scope)

/**
 * Creates a new NodeFire object on the same reference, but with an extended interpolation scope.
 * @param  {Object} scope A dictionary of interpolation variables that will be added to (and take
 *     precedence over) the one carried by this NodeFire object.
 * @return {NodeFire} A new NodeFire object with the same reference and new scope.
 */
scope(scope)

/**
 * Creates a new NodeFire object on a child of this one, and optionally an augmented scope.
 * @param  {string} path The path to the desired child, relative to this reference.  The path will
 *     be interpolated using this object's scope and the additional scope provided.  For the syntax
 *     see the interpolate() method.
 * @param  {Object} scope Optional additional scope that will add to (and override) this object's
 *     scope.
 * @return {NodeFire} A new NodeFire object on the child reference, and with the augmented scope.
 */
child(path, scope)

/**
 * Creates a new NodeFire object on a child of this one, without interpolating the path.  Useful
 * when the path may contain interpolation syntax that must be disregarded, and you've already
 * manually escaped it.
 * @param path The path to the desired child, relative to this reference.  The path will not be
 *     interpolated and must already be escaped, if necessary.
 * @returns {NodeFire} A new NodeFire object on the child reference.
 */
childRaw(path)

/**
 * Gets this reference's current value from Firebase, and inserts it into the cache if a
 * maxCacheSize was set and the `cache` option is not false.
 * @param {Object} [options] Optional operation settings.
 * @param {number} [options.timeout] Operation timeout in milliseconds. Disabled by default.
 * @param {boolean} [options.debugPermissionDenied=true] Set to false to skip diagnostics enabled
 *     by enablePermissionDebugging().
 * @param {boolean} [options.cache=true] Keep the value pinned and updated in the cache if
 *     caching is enabled. Set to false to skip adding this reference to the cache.
 * @return {Promise} A promise that is resolved to the reference's value, or rejected with an error.
 *     The value returned is normalized: arrays are converted to objects, and the value's priority
 *     (if any) is set on a ".priority" attribute if the value is an object.
 */
get(options)

/**
 * Adds this reference to the cache (if maxCacheSize set) and counts a cache hit or miss.
 */
cache()

/**
 * Removes this reference from the cache (if maxCacheSize is set).
 * @return True if the reference was cached, false otherwise.
 */
uncache()

/**
 * Sets the value at this reference.  To set the priority, include a ".priority" attribute on the
 * value.
 * @param {Object || number || string || boolean} value The value to set.
 * @param {Object} [options] Optional operation settings.
 * @param {number} [options.timeout] Operation timeout in milliseconds. Disabled by default.
 * @param {boolean} [options.debugPermissionDenied=true] Set to false to skip diagnostics enabled
 *     by enablePermissionDebugging().
 * @param {boolean} [options.unchecked=false] Skip TypeScript write-shape checking for this
 *     value. Firebase validation still applies.
 * @returns {Promise} A promise that is resolved when the value has been set, or rejected with an
 *     error.
 */
set(value, options)

/**
 * Updates a value at this reference, setting only the top-level keys supplied and leaving any other
 * ones as-is.
 * @param  {Object} value The value to update the reference with.
 * @param {Object} [options] Optional operation settings.
 * @param {number} [options.timeout] Operation timeout in milliseconds. Disabled by default.
 * @param {boolean} [options.debugPermissionDenied=true] Set to false to skip diagnostics enabled
 *     by enablePermissionDebugging().
 * @return {Promise} A promise that is resolved when the value has been updated, or rejected with an
 *     error.
 */
update(value, options)

/**
 * Removes this reference from the Firebase.
 * @param {Object} [options] Optional operation settings.
 * @param {number} [options.timeout] Operation timeout in milliseconds. Disabled by default.
 * @param {boolean} [options.debugPermissionDenied=true] Set to false to skip diagnostics enabled
 *     by enablePermissionDebugging().
 * @return {Promise} A promise that is resolved when the value has been removed, or rejected with an
 *     error.
 */
remove(options)

/**
 * Pushes a value as a new child of this reference, with a new unique key.  Note that if you just
 * want to generate a new unique key you can call newKey() directly.
 * @param  {Object || number || string || boolean} value The value to push.
 * @param {Object} [options] Optional operation settings.
 * @param {number} [options.timeout] Operation timeout in milliseconds. Disabled by default.
 * @param {boolean} [options.debugPermissionDenied=true] Set to false to skip diagnostics enabled
 *     by enablePermissionDebugging().
 * @return {Promise} A promise that is resolved to a new NodeFire object that refers to the newly
 *     pushed value (with the same scope as this object), or rejected with an error.
 */
push(value, options)

/**
 * Runs a transaction at this reference.  The transaction is not applied locally first, since this
 * would be incompatible with a promise's complete-once semantics.
 *
 * There's a bug in the Firebase SDK that fails to update the local value and will cause a
 * transaction to fail repeatedly until it fails with maxretry.  If you specify the detectStuck
 * option then an error with the message 'stuck' will be thrown earlier so that you can try to work
 * around the issue.
 *
 * @param  {function(value):value} updateFunction A function that takes the current value at this
 *     reference and returns the new value to replace it with.  Return undefined to abort the
 *     transaction, and null to remove the reference.  Be prepared for this function to be called
 *     multiple times in case of contention.
 * @param {Object} [options] Optional transaction settings.
 * @param {number} [options.timeout] Transaction timeout in milliseconds, including prefetch.
 *     Disabled by default.
 * @param {boolean} [options.debugPermissionDenied=true] Set to false to skip diagnostics
 *     enabled by enablePermissionDebugging(), including for prefetch failures.
 * @param {number} [options.detectStuck=0] Throw a 'stuck' error after the update function's
 *     input value remains unchanged this many times. Zero disables this check.
 * @param {boolean} [options.prefetchValue=true] Fetch and keep pinned the value referenced
 *     by the transaction while the transaction is in progress.
 * @return {Promise} A promise that is resolved with the (normalized) committed value if the
 *     transaction committed or with undefined if it aborted, or rejected with an error.
 */
transaction(updateFunction, options)

/**
 * Fetches the keys of the current reference's children without also fetching all the contents,
 * using the Firebase REST API.
 *
 * @param {Object} options Fetch settings; all properties are optional.
 * @param {number} [options.maxTries=1] Maximum number of fetch attempts for transient errors.
 * @param {number} [options.retryInterval=1000] Delay between retries in milliseconds.
 * @param {number} [options.timeout] Maximum milliseconds for all fetch attempts and retry
 *     delays. Disabled by default.
 * @return A promise that resolves to an array of key strings.
 */
childrenKeys(options)

/**
 * Generates a unique string that can be used as a key in Firebase.
 * @return {string} A unique string that satisfies Firebase's key syntax constraints.
 */
newKey()

/**
 * The current timestamp after adjusting for the Firebase-computed server time offset.
 */
now

/**
 * Returns whether or not this NodeFire instance is equivalent to the provided NodeFire instance.
 * @return {NodeFire} Another NodeFire instance against which to compare.
 */
isEqual()

/* Some methods that work the same as on Firebase objects. */
toJSON()
toString()

/**
 * Registers a listener. Works the same as on Firebase objects, except that the snapshot passed
 * into the callback (and forEach) is wrapped such that:
 *   1) The val() method will return a normalized method (like NodeFire.get() does).
 *   2) The ref() method will return a NodeFire reference, with the same scope as the reference
 *      on which on() was called.
 *   3) The child() method takes an optional extra scope parameter, just like NodeFire.child().
 * @param {Object} [options] Optional listener settings, passed after context.
 * @param {boolean} [options.debugPermissionDenied=true] Set to false to skip diagnostics
 *     enabled by enablePermissionDebugging() when the listener is cancelled.
 */
on(eventType, callback, cancelCallback, context, options)
off(eventType, callback, context)

/* Query methods which work the same as in the Firebase Admin SDK. */
limitToFirst(limit)
limitToLast(limit)
startAt(value, key)
endAt(value, key)
equalTo(value, key)
orderByChild(path)
orderByKey()
orderByValue()

About

A promise-centric Firebase library for NodeJS

Resources

Stars

10 stars

Watchers

5 watching

Forks

Releases

Packages

Used by

Contributors

Languages