2017-03-02 11:40:08 +00:00
/**
* @flow
*/
import Query from './query.js' ;
import Snapshot from './snapshot' ;
import Disconnect from './disconnect' ;
2017-06-30 17:23:32 +01:00
import ReferenceBase from './../../utils/ReferenceBase' ;
2017-03-02 11:40:08 +00:00
import { promisify , isFunction , isObject , tryJSONParse , tryJSONStringify , generatePushID } from './../../utils' ;
2017-04-26 12:21:53 +01:00
// Unique Reference ID for native events
let refId = 1 ;
2017-03-02 11:40:08 +00:00
/**
2017-05-06 14:33:55 +01:00
* 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").
*
2017-03-10 10:46:55 +00:00
* @link https://firebase.google.com/docs/reference/js/firebase.database.Reference
2017-03-02 11:40:08 +00:00
* @class Reference
2017-05-06 14:33:55 +01:00
* @extends ReferenceBase
2017-03-02 11:40:08 +00:00
*/
export default class Reference extends ReferenceBase {
2017-04-26 12:21:53 +01:00
refId : number ;
2017-05-30 12:46:28 +01:00
refListeners : { [ listenerId : number ] : DatabaseListener };
2017-06-30 17:23:32 +01:00
database : Object ;
2017-03-02 11:40:08 +00:00
query : Query ;
2017-06-30 17:23:32 +01:00
// todo logger missing as reference base no longer extends module base
constructor ( database : Object , path : string , existingModifiers ?: Array < DatabaseModifier > ) {
2017-07-12 15:49:33 +01:00
super ( path , database );
2017-04-26 12:21:53 +01:00
this . refId = refId ++ ;
2017-05-30 12:46:28 +01:00
this . refListeners = {};
2017-03-24 22:53:56 +00:00
this . database = database ;
2017-03-02 11:40:08 +00:00
this . namespace = 'firebase:db:ref' ;
this . query = new Query ( this , path , existingModifiers );
2017-04-26 12:21:53 +01:00
this . log . debug ( 'Created new Reference' , this . refId , this . path );
2017-03-02 11:40:08 +00:00
}
/**
*
* @param bool
* @returns {*}
*/
keepSynced ( bool : boolean ) {
2017-03-24 22:53:56 +00:00
const path = this . path ;
2017-06-30 17:23:32 +01:00
return promisify ( 'keepSynced' , this . database . _native )( path , bool );
2017-03-02 11:40:08 +00:00
}
/**
*
* @param value
* @returns {*}
*/
set ( value : any ) {
2017-03-24 22:53:56 +00:00
const path = this . path ;
2017-03-02 11:40:08 +00:00
const _value = this . _serializeAnyType ( value );
2017-06-30 17:23:32 +01:00
return promisify ( 'set' , this . database . _native )( path , _value );
2017-03-02 11:40:08 +00:00
}
/**
*
* @param val
* @returns {*}
*/
update ( val : Object ) {
2017-03-24 22:53:56 +00:00
const path = this . path ;
2017-03-02 11:40:08 +00:00
const value = this . _serializeObject ( val );
2017-06-30 17:23:32 +01:00
return promisify ( 'update' , this . database . _native )( path , value );
2017-03-02 11:40:08 +00:00
}
/**
*
* @returns {*}
*/
remove () {
2017-06-30 17:23:32 +01:00
return promisify ( 'remove' , this . database . _native )( this . path );
2017-03-02 11:40:08 +00:00
}
/**
*
* @param value
* @param onComplete
* @returns {*}
*/
push ( value : any , onComplete : Function ) {
if ( value === null || value === undefined ) {
2017-03-24 22:53:56 +00:00
return new Reference ( this . database , ` ${ this . path } / ${ generatePushID ( this . database . serverTimeOffset ) } ` );
2017-03-02 11:40:08 +00:00
}
2017-03-24 22:53:56 +00:00
const path = this . path ;
2017-03-02 11:40:08 +00:00
const _value = this . _serializeAnyType ( value );
2017-06-30 17:23:32 +01:00
return promisify ( 'push' , this . database . _native )( path , _value )
2017-03-02 11:40:08 +00:00
. then (({ ref }) => {
2017-03-24 22:53:56 +00:00
const newRef = new Reference ( this . database , ref );
2017-03-02 11:40:08 +00:00
if ( isFunction ( onComplete )) return onComplete ( null , newRef );
return newRef ;
}). catch (( e ) => {
if ( isFunction ( onComplete )) return onComplete ( e , null );
return e ;
});
}
2017-03-07 16:54:04 +00:00
/**
2017-05-09 08:09:03 +01:00
* iOS: Called once with the initial data at the specified location and then once each
* time the data changes. It won't trigger until the entire contents have been
* synchronized.
*
* Android: (@link https://github.com/invertase/react-native-firebase/issues/92)
* - Array & number values: Called once with the initial data at the specified
* location and then twice each time the value changes.
* - Other data types: Called once with the initial data at the specified location
* and once each time the data type changes.
2017-03-07 16:54:04 +00:00
*
2017-05-06 14:33:55 +01:00
* @callback onValueCallback
* @param {!DataSnapshot} dataSnapshot - Snapshot representing data at the location
2017-05-09 08:09:03 +01:00
* specified by the current ref. If location has no data, .val() will return null.
2017-03-07 16:54:04 +00:00
*/
2017-05-06 14:33:55 +01:00
/**
* Called once for each initial child at the specified location and then again
* every time a new child is added.
*
* @callback onChildAddedCallback
* @param {!DataSnapshot} dataSnapshot - Snapshot reflecting the data for the
* relevant child.
* @param {?ReferenceKey} previousChildKey - For ordering purposes, the key
* of the previous sibling child by sort order, or null if it is the first child.
*/
/**
* Called once every time a child is removed.
*
* A child will get removed when either:
* - remove() is explicitly called on a child or one of its ancestors
* - set(null) is called on that child or one of its ancestors
* - a child has all of its children removed
* - there is a query in effect which now filters out the child (because it's sort
* order changed or the max limit was hit)
*
* @callback onChildRemovedCallback
* @param {!DataSnapshot} dataSnapshot - Snapshot reflecting the old data for
* the child that was removed.
*/
/**
* Called when a child (or any of its descendants) changes.
*
* A single child_changed event may represent multiple changes to the child.
*
* @callback onChildChangedCallback
* @param {!DataSnapshot} dataSnapshot - Snapshot reflecting new child contents.
* @param {?ReferenceKey} previousChildKey - For ordering purposes, the key
* of the previous sibling child by sort order, or null if it is the first child.
*/
/**
* Called when a child's sort order changes, i.e. its position relative to its
* siblings changes.
*
* @callback onChildMovedCallback
* @param {!DataSnapshot} dataSnapshot - Snapshot reflecting the data of the moved
* child.
* @param {?ReferenceKey} previousChildKey - For ordering purposes, the key
* of the previous sibling child by sort order, or null if it is the first child.
*/
/**
* @typedef (onValueCallback|onChildAddedCallback|onChildRemovedCallback|onChildChangedCallback|onChildMovedCallback) ReferenceEventCallback
*/
/**
* Called if the event subscription is cancelled because the client does
* not have permission to read this data (or has lost the permission to do so).
*
* @callback onFailureCallback
* @param {Error} error - Object indicating why the failure occurred
*/
/**
* Binds callback handlers to when data changes at the current ref's location.
* The primary method of reading data from a Database.
*
* Callbacks can be unbound using {@link off}.
*
* Event Types:
*
* - value: {@link onValueCallback}.
* - child_added: {@link onChildAddedCallback}
* - child_removed: {@link onChildRemovedCallback}
* - child_changed: {@link onChildChangedCallback}
* - child_moved: {@link onChildMovedCallback}
*
* @param {ReferenceEventType} eventType - Type of event to attach a callback for.
* @param {ReferenceEventCallback} successCallback - Function that will be called
* when the event occurs with the new data.
* @param {onFailureCallback=} failureCallbackOrContext - Optional callback that is called
* if the event subscription fails. {@link onFailureCallback}
* @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 : string , successCallback : () => any , failureCallbackOrContext : () => any , context : any ) {
if ( ! eventType ) throw new Error ( 'Error: Query on failed: Was called with 0 arguments. Expects at least 2' );
if ( ! ReferenceEventTypes [ eventType ]) throw new Error ( 'Query.on failed: First argument must be a valid event type: "value", "child_added", "child_removed", "child_changed", or "child_moved".' );
if ( ! successCallback ) throw new Error ( 'Query.on failed: Was called with 1 argument. Expects at least 2.' );
if ( ! isFunction ( successCallback )) throw new Error ( 'Query.on failed: Second argument must be a valid function.' );
if ( arguments . length > 2 && ! failureCallbackOrContext ) throw new Error ( 'Query.on failed: third argument must either be a cancel callback or a context object.' );
this . log . debug ( 'adding reference.on' , this . refId , eventType );
let _failureCallback ;
let _context ;
if ( context ) {
_context = context ;
_failureCallback = failureCallbackOrContext ;
2017-06-30 17:23:32 +01:00
} else if ( isFunction ( failureCallbackOrContext )) {
_failureCallback = failureCallbackOrContext ;
2017-05-06 14:33:55 +01:00
} else {
2017-06-30 17:23:32 +01:00
_context = failureCallbackOrContext ;
2017-05-06 14:33:55 +01:00
}
if ( _failureCallback ) {
_failureCallback = ( error ) => {
if ( error . message . startsWith ( 'FirebaseError: permission_denied' )) {
2017-07-12 13:39:29 +01:00
// eslint-disable-next-line
2017-06-30 17:23:32 +01:00
error . message = `permission_denied at / ${ this . path } : Client doesn't have permission to access the desired data.`
2017-05-06 14:33:55 +01:00
}
failureCallbackOrContext ( error );
2017-06-30 17:23:32 +01:00
};
2017-05-06 14:33:55 +01:00
}
let _successCallback ;
if ( _context ) {
_successCallback = successCallback . bind ( _context );
} else {
_successCallback = successCallback ;
}
2017-04-26 12:21:53 +01:00
const listener = {
2017-05-30 12:46:28 +01:00
listenerId : Object . keys ( this . refListeners ). length + 1 ,
2017-05-06 14:33:55 +01:00
eventName : eventType ,
successCallback : _successCallback ,
failureCallback : _failureCallback ,
2017-04-26 12:21:53 +01:00
};
2017-05-06 14:33:55 +01:00
2017-05-30 12:46:28 +01:00
this . refListeners [ listener . listenerId ] = listener ;
2017-04-26 12:21:53 +01:00
this . database . on ( this , listener );
2017-03-24 22:53:56 +00:00
return successCallback ;
2017-03-02 11:40:08 +00:00
}
2017-03-07 16:54:04 +00:00
/**
*
2017-04-26 12:21:53 +01:00
* @param eventName
2017-03-07 16:54:04 +00:00
* @param successCallback
* @param failureCallback
2017-07-12 13:39:29 +01:00
* TODO @param context
2017-03-07 16:54:04 +00:00
* @returns {Promise.<TResult>}
*/
2017-04-26 12:21:53 +01:00
once ( eventName : string = 'value' , successCallback : ( snapshot : Object ) => void , failureCallback : ( error : FirebaseError ) => void ) {
2017-06-30 17:23:32 +01:00
return promisify ( 'once' , this . database . _native )( this . refId , this . path , this . query . getModifiers (), eventName )
2017-03-02 11:40:08 +00:00
. then (({ snapshot }) => new Snapshot ( this , snapshot ))
. then (( snapshot ) => {
2017-03-07 16:54:04 +00:00
if ( isFunction ( successCallback )) successCallback ( snapshot );
2017-03-02 11:40:08 +00:00
return snapshot ;
2017-03-07 16:54:04 +00:00
})
. catch (( error ) => {
2017-03-24 22:53:56 +00:00
const firebaseError = this . database . _toFirebaseError ( error );
2017-03-07 17:35:48 +00:00
if ( isFunction ( failureCallback )) return failureCallback ( firebaseError );
2017-03-07 16:58:33 +00:00
return Promise . reject ( firebaseError );
2017-03-02 11:40:08 +00:00
});
}
2017-03-07 17:35:48 +00:00
/**
2017-05-06 14:33:55 +01:00
* Detaches a callback attached with on().
2017-03-07 17:35:48 +00:00
*
2017-05-06 14:33:55 +01:00
* Calling off() on a parent listener will not automatically remove listeners
* registered on child nodes.
*
* If on() was called multiple times with the same eventType off() must be
* called multiple times to completely remove it.
*
* If a callback is not specified, all callbacks for the specified eventType
* will be removed. If no eventType or callback is specified, all callbacks
* for the Reference will be removed.
*
* If a context is specified, it too is used as a filter parameter: a callback
* will only be detached if, when it was attached with on(), the same event type,
* callback function and context were provided.
*
* If no callbacks matching the parameters provided are found, no callbacks are
* detached.
*
* @param {('value'|'child_added'|'child_changed'|'child_removed'|'child_moved')=} eventType - Type of event to detach callback for.
* @param {Function=} originalCallback - Original callback passed to on()
2017-07-12 13:39:29 +01:00
* TODO @param {*=} context - The context passed to on() when the callback was bound
2017-05-06 14:33:55 +01:00
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#off}
2017-03-07 17:35:48 +00:00
*/
2017-05-06 14:33:55 +01:00
off ( eventType ?: string = '' , originalCallback ?: () => any ) {
this . log . debug ( 'ref.off(): ' , this . refId , eventType );
2017-04-28 10:27:29 +01:00
// $FlowFixMe
2017-05-30 12:46:28 +01:00
const listeners : Array < DatabaseListener > = Object . values ( this . refListeners );
2017-04-26 12:21:53 +01:00
let listenersToRemove ;
2017-05-06 14:33:55 +01:00
if ( eventType && originalCallback ) {
2017-04-28 10:27:29 +01:00
listenersToRemove = listeners . filter (( listener ) => {
2017-05-06 14:33:55 +01:00
return listener . eventName === eventType && listener . successCallback === originalCallback ;
2017-04-26 12:21:53 +01:00
});
// Only remove a single listener as per the web spec
if ( listenersToRemove . length > 1 ) listenersToRemove = [ listenersToRemove [ 0 ]];
2017-05-06 14:33:55 +01:00
} else if ( eventType ) {
2017-04-28 10:27:29 +01:00
listenersToRemove = listeners . filter (( listener ) => {
2017-05-06 14:33:55 +01:00
return listener . eventName === eventType ;
2017-04-26 12:21:53 +01:00
});
2017-05-06 14:33:55 +01:00
} else if ( originalCallback ) {
2017-04-28 10:27:29 +01:00
listenersToRemove = listeners . filter (( listener ) => {
2017-05-06 14:33:55 +01:00
return listener . successCallback === originalCallback ;
2017-04-26 12:21:53 +01:00
});
} else {
2017-04-28 10:27:29 +01:00
listenersToRemove = listeners ;
2017-04-26 12:21:53 +01:00
}
// Remove the listeners from the reference to prevent memory leaks
listenersToRemove . forEach (( listener ) => {
2017-05-30 12:46:28 +01:00
delete this . refListeners [ listener . listenerId ];
2017-04-26 12:21:53 +01:00
});
2017-05-30 12:46:28 +01:00
return this . database . off ( this . refId , listenersToRemove , Object . keys ( this . refListeners ). length );
2017-03-24 22:53:56 +00:00
}
/**
* Atomically modifies the data at this location.
* @url https://firebase.google.com/docs/reference/js/firebase.database.Reference#transaction
* @param transactionUpdate
* @param onComplete
* @param applyLocally
*/
2017-07-12 13:39:29 +01:00
2017-06-30 17:23:32 +01:00
transaction ( transactionUpdate : Function , onComplete : ( error : ? Error , committed : boolean , snapshot : ? Snapshot ) => * , applyLocally : boolean = false ) {
2017-05-10 17:37:03 +01:00
if ( ! isFunction ( transactionUpdate )) {
return Promise . reject (
2017-06-30 17:23:32 +01:00
new Error ( 'Missing transactionUpdate function argument.' ),
2017-05-10 17:37:03 +01:00
);
}
2017-03-24 22:53:56 +00:00
return new Promise (( resolve , reject ) => {
const onCompleteWrapper = ( error , committed , snapshotData ) => {
if ( error ) {
2017-04-04 17:58:20 +01:00
if ( typeof onComplete === 'function' ) {
onComplete ( error , committed , null );
}
2017-03-24 22:53:56 +00:00
return reject ( error );
}
const snapshot = new Snapshot ( this , snapshotData );
2017-04-04 17:58:20 +01:00
if ( typeof onComplete === 'function' ) {
2017-03-24 22:53:56 +00:00
onComplete ( null , committed , snapshot );
}
return resolve ({ committed , snapshot });
};
this . database . transaction . add ( this , transactionUpdate , onCompleteWrapper , applyLocally );
});
2017-03-02 11:40:08 +00:00
}
/**
* MODIFIERS
*/
/**
*
* @returns {Reference}
*/
orderByKey () : Reference {
return this . orderBy ( 'orderByKey' );
}
/**
*
* @returns {Reference}
*/
orderByPriority () : Reference {
return this . orderBy ( 'orderByPriority' );
}
/**
*
* @returns {Reference}
*/
orderByValue () : Reference {
return this . orderBy ( 'orderByValue' );
}
/**
*
* @param key
* @returns {Reference}
*/
orderByChild ( key : string ) : Reference {
return this . orderBy ( 'orderByChild' , key );
}
/**
*
* @param name
* @param key
* @returns {Reference}
*/
orderBy ( name : string , key ?: string ) : Reference {
2017-03-24 22:53:56 +00:00
const newRef = new Reference ( this . database , this . path , this . query . getModifiers ());
2017-04-26 12:21:53 +01:00
newRef . query . orderBy ( name , key );
2017-03-02 11:40:08 +00:00
return newRef ;
}
/**
* LIMITS
*/
/**
*
* @param limit
* @returns {Reference}
*/
limitToLast ( limit : number ) : Reference {
return this . limit ( 'limitToLast' , limit );
}
/**
*
* @param limit
* @returns {Reference}
*/
limitToFirst ( limit : number ) : Reference {
return this . limit ( 'limitToFirst' , limit );
}
/**
*
* @param name
* @param limit
* @returns {Reference}
*/
limit ( name : string , limit : number ) : Reference {
2017-03-24 22:53:56 +00:00
const newRef = new Reference ( this . database , this . path , this . query . getModifiers ());
2017-04-26 12:21:53 +01:00
newRef . query . limit ( name , limit );
2017-03-02 11:40:08 +00:00
return newRef ;
}
/**
* FILTERS
*/
/**
*
* @param value
* @param key
* @returns {Reference}
*/
equalTo ( value : any , key ?: string ) : Reference {
return this . filter ( 'equalTo' , value , key );
}
/**
*
* @param value
* @param key
* @returns {Reference}
*/
endAt ( value : any , key ?: string ) : Reference {
return this . filter ( 'endAt' , value , key );
}
/**
*
* @param value
* @param key
* @returns {Reference}
*/
startAt ( value : any , key ?: string ) : Reference {
return this . filter ( 'startAt' , value , key );
}
/**
*
* @param name
* @param value
* @param key
* @returns {Reference}
*/
filter ( name : string , value : any , key ?: string ) : Reference {
2017-03-24 22:53:56 +00:00
const newRef = new Reference ( this . database , this . path , this . query . getModifiers ());
2017-04-26 12:21:53 +01:00
newRef . query . filter ( name , value , key );
2017-03-02 11:40:08 +00:00
return newRef ;
}
2017-03-08 13:21:21 +00:00
/**
*
* @returns {Disconnect}
*/
2017-03-02 11:40:08 +00:00
onDisconnect () {
return new Disconnect ( this . path );
}
2017-03-08 13:21:21 +00:00
/**
2017-05-06 14:33:55 +01:00
* 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}
2017-03-08 13:21:21 +00:00
*/
2017-03-02 11:40:08 +00:00
child ( path : string ) {
2017-03-24 22:53:56 +00:00
return new Reference ( this . database , ` ${ this . path } / ${ path } ` );
2017-03-02 11:40:08 +00:00
}
2017-03-08 13:21:21 +00:00
/**
* Return the ref as a path string
* @returns {string}
*/
2017-03-02 11:40:08 +00:00
toString () : string {
2017-03-24 22:53:56 +00:00
return this . path ;
2017-03-02 11:40:08 +00:00
}
2017-04-22 17:59:04 +01:00
/**
* 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
2017-05-06 14:33:55 +01:00
*
2017-04-22 17:59:04 +01:00
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#isEqual}
*/
isEqual ( otherRef : Reference ) : boolean {
return !! otherRef && otherRef . constructor === Reference && otherRef . key === this . key ;
}
2017-03-02 11:40:08 +00:00
/**
* GETTERS
*/
/**
2017-05-06 14:33:55 +01:00
* 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}
2017-03-02 11:40:08 +00:00
*/
2017-06-30 17:23:32 +01:00
get parent () : Reference | null {
2017-03-02 11:40:08 +00:00
if ( this . path === '/' ) return null ;
2017-03-24 22:53:56 +00:00
return new Reference ( this . database , this . path . substring ( 0 , this . path . lastIndexOf ( '/' )));
2017-03-02 11:40:08 +00:00
}
2017-04-22 09:27:37 +01:00
/**
* A reference to itself
* @type {!Reference}
2017-05-06 14:33:55 +01:00
*
2017-04-22 09:27:37 +01:00
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#ref}
*/
get ref () : Reference {
return this ;
}
2017-03-02 11:40:08 +00:00
/**
2017-05-06 14:33:55 +01:00
* Reference to the root of the database: '/'
* @type {!Reference}
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#root}
2017-03-02 11:40:08 +00:00
*/
get root () : Reference {
2017-03-24 22:53:56 +00:00
return new Reference ( this . database , '/' );
2017-03-02 11:40:08 +00:00
}
/**
* INTERNALS
*/
/**
*
* @param obj
* @returns {Object}
* @private
*/
_serializeObject ( obj : Object ) {
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 : any ) {
if ( isObject ( value )) {
return {
type : 'object' ,
value : this . _serializeObject ( value ),
};
}
return {
type : typeof value ,
value ,
};
}
}