react-native-firebase-for-netmera
Version:
714 lines (589 loc) • 20.2 kB
JavaScript
/**
*
* Database Reference representation wrapper
*/
import Query from './Query';
import DataSnapshot from './DataSnapshot';
import OnDisconnect from './OnDisconnect';
import { getLogger } from '../../utils/log';
import { getNativeModule } from '../../utils/native';
import ReferenceBase from '../../utils/ReferenceBase';
import { promiseOrCallback, isFunction, isObject, isString, tryJSONParse, tryJSONStringify, generatePushID } from '../../utils';
import SyncTree from '../../utils/SyncTree';
// track all event registrations by path
let listeners = 0;
/**
* Enum for event types
* @readonly
* @enum {String}
*/
const ReferenceEventTypes = {
value: 'value',
child_added: 'child_added',
child_removed: 'child_removed',
child_changed: 'child_changed',
child_moved: 'child_moved'
};
/**
* @typedef {String} ReferenceLocation - Path to location in the database, relative
* to the root reference. Consists of a path where segments are separated by a
* forward slash (/) and ends in a ReferenceKey - except the root location, which
* has no ReferenceKey.
*
* @example
* // root reference location: '/'
* // non-root reference: '/path/to/referenceKey'
*/
/**
* @typedef {String} ReferenceKey - Identifier for each location that is unique to that
* location, within the scope of its parent. The last part of a ReferenceLocation.
*/
/**
* Represents a specific location in your Database that can be used for
* reading or writing data.
*
* You can reference the root using firebase.database().ref() or a child location
* by calling firebase.database().ref("child/path").
*
* @link https://firebase.google.com/docs/reference/js/firebase.database.Reference
* @class Reference
* @extends ReferenceBase
*/
export default class Reference extends ReferenceBase {
constructor(database, path, existingModifiers) {
super(path);
this._refListeners = {};
this._database = database;
this._query = new Query(this, existingModifiers);
getLogger(database).debug('Created new Reference', this._getRefKey());
}
/**
* By calling `keepSynced(true)` on a location, the data for that location will
* automatically be downloaded and kept in sync, even when no listeners are
* attached for that location. Additionally, while a location is kept synced,
* it will not be evicted from the persistent disk cache.
*
* @link https://firebase.google.com/docs/reference/android/com/google/firebase/database/Query.html#keepSynced(boolean)
* @param bool
* @returns {*}
*/
keepSynced(bool) {
return getNativeModule(this._database).keepSynced(this._getRefKey(), this.path, this._query.getModifiers(), bool);
}
/**
* Writes data to this Database location.
*
* @link https://firebase.google.com/docs/reference/js/firebase.database.Reference#set
* @param value
* @param onComplete
* @returns {Promise}
*/
set(value, onComplete) {
return promiseOrCallback(getNativeModule(this._database).set(this.path, this._serializeAnyType(value)), onComplete);
}
/**
* Sets a priority for the data at this Database location.
*
* @link https://firebase.google.com/docs/reference/js/firebase.database.Reference#setPriority
* @param priority
* @param onComplete
* @returns {Promise}
*/
setPriority(priority, onComplete) {
const _priority = this._serializeAnyType(priority);
return promiseOrCallback(getNativeModule(this._database).setPriority(this.path, _priority), onComplete);
}
/**
* Writes data the Database location. Like set() but also specifies the priority for that data.
*
* @link https://firebase.google.com/docs/reference/js/firebase.database.Reference#setWithPriority
* @param value
* @param priority
* @param onComplete
* @returns {Promise}
*/
setWithPriority(value, priority, onComplete) {
const _value = this._serializeAnyType(value);
const _priority = this._serializeAnyType(priority);
return promiseOrCallback(getNativeModule(this._database).setWithPriority(this.path, _value, _priority), onComplete);
}
/**
* Writes multiple values to the Database at once.
*
* @link https://firebase.google.com/docs/reference/js/firebase.database.Reference#update
* @param val
* @param onComplete
* @returns {Promise}
*/
update(val, onComplete) {
const value = this._serializeObject(val);
return promiseOrCallback(getNativeModule(this._database).update(this.path, value), onComplete);
}
/**
* Removes the data at this Database location.
*
* @link https://firebase.google.com/docs/reference/js/firebase.database.Reference#remove
* @param onComplete
* @return {Promise}
*/
remove(onComplete) {
return promiseOrCallback(getNativeModule(this._database).remove(this.path), onComplete);
}
/**
* Atomically modifies the data at this location.
*
* @link https://firebase.google.com/docs/reference/js/firebase.database.Reference#transaction
* @param transactionUpdate
* @param onComplete
* @param applyLocally
*/
transaction(transactionUpdate, onComplete, applyLocally = false) {
if (!isFunction(transactionUpdate)) {
return Promise.reject(new Error('Missing transactionUpdate function argument.'));
}
return new Promise((resolve, reject) => {
const onCompleteWrapper = (error, committed, snapshotData) => {
if (isFunction(onComplete)) {
if (error) {
onComplete(error, committed, null);
} else {
onComplete(null, committed, new DataSnapshot(this, snapshotData));
}
}
if (error) return reject(error);
return resolve({
committed,
snapshot: new DataSnapshot(this, snapshotData)
});
}; // start the transaction natively
this._database._transactionHandler.add(this, transactionUpdate, onCompleteWrapper, applyLocally);
});
}
/**
*
* @param eventName
* @param successCallback
* @param cancelOrContext
* @param context
* @returns {Promise.<any>}
*/
once(eventName = 'value', successCallback, cancelOrContext, context) {
return getNativeModule(this._database).once(this._getRefKey(), this.path, this._query.getModifiers(), eventName).then(({
snapshot
}) => {
const _snapshot = new DataSnapshot(this, snapshot);
if (isFunction(successCallback)) {
if (isObject(cancelOrContext)) successCallback.bind(cancelOrContext)(_snapshot);
if (context && isObject(context)) successCallback.bind(context)(_snapshot);
successCallback(_snapshot);
}
return _snapshot;
}).catch(error => {
if (isFunction(cancelOrContext)) return cancelOrContext(error);
throw error;
});
}
/**
*
* @param value
* @param onComplete
* @returns {*}
*/
push(value, onComplete) {
const name = generatePushID(this._database._serverTimeOffset);
const pushRef = this.child(name);
const thennablePushRef = this.child(name);
let promise;
if (value != null) {
promise = thennablePushRef.set(value, onComplete).then(() => pushRef);
} else {
promise = Promise.resolve(pushRef);
}
thennablePushRef.then = promise.then.bind(promise);
thennablePushRef.catch = promise.catch.bind(promise);
if (isFunction(onComplete)) {
promise.catch(() => {});
}
return thennablePushRef;
}
/**
* MODIFIERS
*/
/**
*
* @returns {Reference}
*/
orderByKey() {
return this.orderBy('orderByKey');
}
/**
*
* @returns {Reference}
*/
orderByPriority() {
return this.orderBy('orderByPriority');
}
/**
*
* @returns {Reference}
*/
orderByValue() {
return this.orderBy('orderByValue');
}
/**
*
* @param key
* @returns {Reference}
*/
orderByChild(key) {
return this.orderBy('orderByChild', key);
}
/**
*
* @param name
* @param key
* @returns {Reference}
*/
orderBy(name, key) {
const newRef = new Reference(this._database, this.path, this._query.getModifiers());
newRef._query.orderBy(name, key);
return newRef;
}
/**
* LIMITS
*/
/**
*
* @param limit
* @returns {Reference}
*/
limitToLast(limit) {
return this.limit('limitToLast', limit);
}
/**
*
* @param limit
* @returns {Reference}
*/
limitToFirst(limit) {
return this.limit('limitToFirst', limit);
}
/**
*
* @param name
* @param limit
* @returns {Reference}
*/
limit(name, limit) {
const newRef = new Reference(this._database, this.path, this._query.getModifiers());
newRef._query.limit(name, limit);
return newRef;
}
/**
* FILTERS
*/
/**
*
* @param value
* @param key
* @returns {Reference}
*/
equalTo(value, key) {
return this.filter('equalTo', value, key);
}
/**
*
* @param value
* @param key
* @returns {Reference}
*/
endAt(value, key) {
return this.filter('endAt', value, key);
}
/**
*
* @param value
* @param key
* @returns {Reference}
*/
startAt(value, key) {
return this.filter('startAt', value, key);
}
/**
*
* @param name
* @param value
* @param key
* @returns {Reference}
*/
filter(name, value, key) {
const newRef = new Reference(this._database, this.path, this._query.getModifiers());
newRef._query.filter(name, value, key);
return newRef;
}
/**
*
* @returns {OnDisconnect}
*/
onDisconnect() {
return new OnDisconnect(this);
}
/**
* Creates a Reference to a child of the current Reference, using a relative path.
* No validation is performed on the path to ensure it has a valid format.
* @param {String} path relative to current ref's location
* @returns {!Reference} A new Reference to the path provided, relative to the current
* Reference
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#child}
*/
child(path) {
return new Reference(this._database, `${this.path}/${path}`);
}
/**
* Return the ref as a path string
* @returns {string}
*/
toString() {
return `${this._database.databaseUrl}${this.path}`;
}
/**
* Return a JSON-serializable representation of this object.
* @returns {string}
*/
toJSON() {
return this.toString();
}
/**
* Returns whether another Reference represent the same location and are from the
* same instance of firebase.app.App - multiple firebase apps not currently supported.
* @param {Reference} otherRef - Other reference to compare to this one
* @return {Boolean} Whether otherReference is equal to this one
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#isEqual}
*/
isEqual(otherRef) {
return !!otherRef && otherRef.constructor === Reference && otherRef.key === this.key && this._query.queryIdentifier() === otherRef._query.queryIdentifier();
}
/**
* GETTERS
*/
/**
* The parent location of a Reference, or null for the root Reference.
* @type {Reference}
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#parent}
*/
get parent() {
if (this.path === '/') return null;
return new Reference(this._database, this.path.substring(0, this.path.lastIndexOf('/')));
}
/**
* A reference to itself
* @type {!Reference}
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#ref}
*/
get ref() {
return this;
}
/**
* Reference to the root of the database: '/'
* @type {!Reference}
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#root}
*/
get root() {
return new Reference(this._database, '/');
}
/**
* INTERNALS
*/
/**
* Generate a unique registration key.
*
* @return {string}
*/
_getRegistrationKey(eventType) {
return `$${this._database.databaseUrl}$/${this.path}$${this._query.queryIdentifier()}$${listeners}$${eventType}`;
}
/**
* Generate a string that uniquely identifies this
* combination of path and query modifiers
*
* @return {string}
* @private
*/
_getRefKey() {
return `$${this._database.databaseUrl}$/${this.path}$${this._query.queryIdentifier()}`;
}
/**
*
* @param obj
* @returns {Object}
* @private
*/
_serializeObject(obj) {
if (!isObject(obj)) return obj; // json stringify then parse it calls toString on Objects / Classes
// that support it i.e new Date() becomes a ISO string.
return tryJSONParse(tryJSONStringify(obj));
}
/**
*
* @param value
* @returns {*}
* @private
*/
_serializeAnyType(value) {
if (isObject(value)) {
return {
type: 'object',
value: this._serializeObject(value)
};
}
return {
type: typeof value,
value
};
}
/**
* Register a listener for data changes at the current ref's location.
* The primary method of reading data from a Database.
*
* Listeners can be unbound using {@link off}.
*
* Event Types:
*
* - value: {@link callback}.
* - child_added: {@link callback}
* - child_removed: {@link callback}
* - child_changed: {@link callback}
* - child_moved: {@link callback}
*
* @param {ReferenceEventType} eventType - Type of event to attach a callback for.
* @param {ReferenceEventCallback} callback - Function that will be called
* when the event occurs with the new data.
* @param {cancelCallbackOrContext=} cancelCallbackOrContext - Optional callback that is called
* if the event subscription fails. {@link cancelCallbackOrContext}
* @param {*=} context - Optional object to bind the callbacks to when calling them.
* @returns {ReferenceEventCallback} callback function, unmodified (unbound), for
* convenience if you want to pass an inline function to on() and store it later for
* removing using off().
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#on}
*/
on(eventType, callback, cancelCallbackOrContext, context) {
if (!eventType) {
throw new Error('Query.on failed: Function called with 0 arguments. Expects at least 2.');
}
if (!isString(eventType) || !ReferenceEventTypes[eventType]) {
throw new Error(`Query.on failed: First argument must be a valid string event type: "${Object.keys(ReferenceEventTypes).join(', ')}"`);
}
if (!callback) {
throw new Error('Query.on failed: Function called with 1 argument. Expects at least 2.');
}
if (!isFunction(callback)) {
throw new Error('Query.on failed: Second argument must be a valid function.');
}
if (cancelCallbackOrContext && !isFunction(cancelCallbackOrContext) && !isObject(context) && !isObject(cancelCallbackOrContext)) {
throw new Error('Query.on failed: Function called with 3 arguments, but third optional argument `cancelCallbackOrContext` was not a function.');
}
if (cancelCallbackOrContext && !isFunction(cancelCallbackOrContext) && context) {
throw new Error('Query.on failed: Function called with 4 arguments, but third optional argument `cancelCallbackOrContext` was not a function.');
}
const eventRegistrationKey = this._getRegistrationKey(eventType);
const registrationCancellationKey = `${eventRegistrationKey}$cancelled`;
const _context = cancelCallbackOrContext && !isFunction(cancelCallbackOrContext) ? cancelCallbackOrContext : context;
const registrationObj = {
eventType,
ref: this,
path: this.path,
key: this._getRefKey(),
appName: this._database.app.name,
dbURL: this._database.databaseUrl,
eventRegistrationKey
};
SyncTree.addRegistration({ ...registrationObj,
listener: _context ? callback.bind(_context) : callback
});
if (cancelCallbackOrContext && isFunction(cancelCallbackOrContext)) {
// cancellations have their own separate registration
// as these are one off events, and they're not guaranteed
// to occur either, only happens on failure to register on native
SyncTree.addRegistration({
ref: this,
once: true,
path: this.path,
key: this._getRefKey(),
appName: this._database.app.name,
dbURL: this._database.databaseUrl,
eventType: `${eventType}$cancelled`,
eventRegistrationKey: registrationCancellationKey,
listener: _context ? cancelCallbackOrContext.bind(_context) : cancelCallbackOrContext
});
} // initialise the native listener if not already listening
getNativeModule(this._database).on({
eventType,
path: this.path,
key: this._getRefKey(),
appName: this._database.app.name,
modifiers: this._query.getModifiers(),
hasCancellationCallback: isFunction(cancelCallbackOrContext),
registration: {
eventRegistrationKey,
key: registrationObj.key,
registrationCancellationKey
}
}); // increment number of listeners - just a short way of making
// every registration unique per .on() call
listeners += 1; // return original unbound successCallback for
// the purposes of calling .off(eventType, callback) at a later date
return callback;
}
/**
* Detaches a callback previously attached with on().
*
* Detach a callback previously attached with on(). Note that if on() was called
* multiple times with the same eventType and callback, the callback will be called
* multiple times for each event, and off() must be called multiple times to
* remove the callback. Calling off() on a parent listener will not automatically
* remove listeners registered on child nodes, off() must also be called on any
* child listeners to remove the callback.
*
* If a callback is not specified, all callbacks for the specified eventType will be removed.
* Similarly, if no eventType or callback is specified, all callbacks for the Reference will be removed.
* @param eventType
* @param originalCallback
*/
off(eventType = '', originalCallback) {
if (!arguments.length) {
// Firebase Docs:
// if no eventType or callback is specified, all callbacks for the Reference will be removed.
return SyncTree.removeListenersForRegistrations(SyncTree.getRegistrationsByPath(this.path));
}
/*
* VALIDATE ARGS
*/
if (eventType && (!isString(eventType) || !ReferenceEventTypes[eventType])) {
throw new Error(`Query.off failed: First argument must be a valid string event type: "${Object.keys(ReferenceEventTypes).join(', ')}"`);
}
if (originalCallback && !isFunction(originalCallback)) {
throw new Error('Query.off failed: Function called with 2 arguments, but second optional argument was not a function.');
} // Firebase Docs:
// Note that if on() was called
// multiple times with the same eventType and callback, the callback will be called
// multiple times for each event, and off() must be called multiple times to
// remove the callback.
// Remove only a single registration
if (eventType && originalCallback) {
const registration = SyncTree.getOneByPathEventListener(this.path, eventType, originalCallback);
if (!registration) return []; // remove the paired cancellation registration if any exist
SyncTree.removeListenersForRegistrations([`${registration}$cancelled`]); // remove only the first registration to match firebase web sdk
// call multiple times to remove multiple registrations
return SyncTree.removeListenerRegistrations(originalCallback, [registration]);
} // Firebase Docs:
// If a callback is not specified, all callbacks for the specified eventType will be removed.
const registrations = SyncTree.getRegistrationsByPathEvent(this.path, eventType);
SyncTree.removeListenersForRegistrations(SyncTree.getRegistrationsByPathEvent(this.path, `${eventType}$cancelled`));
return SyncTree.removeListenersForRegistrations(registrations);
}
}