2
0
mirror of synced 2025-01-11 06:35:51 +00:00

RNFirebase - database - js

This commit is contained in:
Salakar 2017-03-02 11:40:08 +00:00
parent 4d9255c3e9
commit 6a9b54124a
4 changed files with 701 additions and 0 deletions

View File

@ -0,0 +1,229 @@
/**
* @flow
* Database representation wrapper
*/
import { NativeModules, NativeEventEmitter } from 'react-native';
import { Base } from './../base';
import Snapshot from './snapshot.js';
import Reference from './reference.js';
import { promisify } from './../../utils';
const FirebaseDatabase = NativeModules.FirebaseDatabase;
const FirebaseDatabaseEvt = new NativeEventEmitter(FirebaseDatabase);
/**
* @class Database
*/
export default class Database extends Base {
constructor(firebase: Object, options: Object = {}) {
super(firebase, options);
this.subscriptions = {};
this.errorSubscriptions = {};
this.serverTimeOffset = 0;
this.persistenceEnabled = false;
this.namespace = 'firebase:database';
if (firebase.options.persistence === true) {
this._setPersistence(true);
}
this.successListener = FirebaseDatabaseEvt.addListener(
'database_event',
event => this._handleDatabaseEvent(event)
);
this.errorListener = FirebaseDatabaseEvt.addListener(
'database_error',
err => this._handleDatabaseError(err)
);
this.offsetRef = this.ref('.info/serverTimeOffset');
this.offsetRef.on('value', (snapshot) => {
this.serverTimeOffset = snapshot.val() || this.serverTimeOffset;
});
this.log.debug('Created new Database instance', this.options);
}
/**
* https://firebase.google.com/docs/reference/js/firebase.database.ServerValue
* @returns {{TIMESTAMP: (*|{[.sv]: string})}}
* @constructor
*/
get ServerValue(): Object {
return {
TIMESTAMP: FirebaseDatabase.serverValueTimestamp || { '.sv': 'timestamp' },
};
}
/**
* Returns a new firebase reference instance
* @param path
* @returns {Reference}
*/
ref(path: string) {
return new Reference(this, path);
}
/**
*
* @param path
* @param modifiersString
* @param modifiers
* @param eventName
* @param cb
* @param errorCb
* @returns {*}
*/
on(path: string, modifiersString: string, modifiers: Array<string>, eventName: string, cb: () => void, errorCb: () => void) {
const handle = this._handle(path, modifiersString);
this.log.debug('adding on listener', handle);
if (!this.subscriptions[handle]) this.subscriptions[handle] = {};
if (!this.subscriptions[handle][eventName]) this.subscriptions[handle][eventName] = [];
this.subscriptions[handle][eventName].push(cb);
if (errorCb) {
if (!this.errorSubscriptions[handle]) this.errorSubscriptions[handle] = [];
this.errorSubscriptions[handle].push(errorCb);
}
return promisify('on', FirebaseDatabase)(path, modifiersString, modifiers, eventName);
}
/**
*
* @param path
* @param modifiersString
* @param eventName
* @param origCB
* @returns {*}
*/
off(path: string, modifiersString: string, eventName?: string, origCB?: () => void) {
const handle = this._handle(path, modifiersString);
this.log.debug('off() : ', handle, eventName);
if (!this.subscriptions[handle] || (eventName && !this.subscriptions[handle][eventName])) {
this.log.warn('off() called, but not currently listening at that location (bad path)', handle, eventName);
return Promise.resolve();
}
if (eventName && origCB) {
const i = this.subscriptions[handle][eventName].indexOf(origCB);
if (i === -1) {
this.log.warn('off() called, but the callback specified is not listening at that location (bad path)', handle, eventName);
return Promise.resolve();
}
this.subscriptions[handle][eventName].splice(i, 1);
if (this.subscriptions[handle][eventName].length > 0) return Promise.resolve();
} else if (eventName) {
this.subscriptions[handle][eventName] = [];
} else {
this.subscriptions[handle] = {};
}
this.errorSubscriptions[handle] = [];
return promisify('off', FirebaseDatabase)(path, modifiersString, eventName);
}
/**
* Removes all event handlers and their native subscriptions
* @returns {Promise.<*>}
*/
cleanup() {
const promises = [];
Object.keys(this.subscriptions).forEach((handle) => {
Object.keys(this.subscriptions[handle]).forEach((eventName) => {
const separator = handle.indexOf('|');
const path = handle.substring(0, separator);
const modifiersString = handle.substring(separator + 1);
promises.push(this.off(path, modifiersString, eventName));
});
});
return Promise.all(promises);
}
goOnline() {
FirebaseDatabase.goOnline();
}
goOffline() {
FirebaseDatabase.goOffline();
}
/**
* INTERNALS
*/
_getServerTime() {
return new Date().getTime() + this.serverTimeOffset;
}
/**
* Enabled / disable database persistence
* @param enable
* @returns {*}
* @private
*/
_setPersistence(enable: boolean = true) {
if (this.persistenceEnabled !== enable) {
this.log.debug(`${enable ? 'Enabling' : 'Disabling'} persistence`);
this.persistenceEnabled = enable;
return this.whenReady(promisify('enablePersistence', FirebaseDatabase)(enable));
}
return this.whenReady(Promise.resolve({ status: 'Already enabled' }));
}
/**
*
* @param path
* @param modifiersString
* @returns {string}
* @private
*/
_handle(path: string = '', modifiersString: string = '') {
return `${path}|${modifiersString}`;
}
/**
*
* @param event
* @private
*/
_handleDatabaseEvent(event: Object) {
const body = event.body || {};
const { path, modifiersString, eventName, snapshot } = body;
const handle = this._handle(path, modifiersString);
this.log.debug('_handleDatabaseEvent: ', handle, eventName, snapshot && snapshot.key);
if (this.subscriptions[handle] && this.subscriptions[handle][eventName]) {
this.subscriptions[handle][eventName].forEach((cb) => {
cb(new Snapshot(new Reference(this, path, modifiersString.split('|')), snapshot), body);
});
} else {
FirebaseDatabase.off(path, modifiersString, eventName, () => {
this.log.debug('_handleDatabaseEvent: No JS listener registered, removed native listener', handle, eventName);
});
}
}
/**
*
* @param err
* @private
*/
_handleDatabaseError(err: Object) {
const body = err.body || {};
const { path, modifiersString, eventName, msg } = body;
const handle = this._handle(path, modifiersString);
this.log.debug('_handleDatabaseError ->', handle, eventName, err);
if (this.errorSubscriptions[handle]) this.errorSubscriptions[handle].forEach((cb) => cb(new Error(msg)));
}
}

View File

@ -0,0 +1,55 @@
/**
* @flow
*/
import { ReferenceBase } from './../base';
import Reference from './reference.js';
/**
* @class Query
*/
export default class Query extends ReferenceBase {
static ref: Reference;
static modifiers: Array<string>;
ref: Reference;
constructor(ref: Reference, path: string, existingModifiers?: Array<string>) {
super(ref.db, path);
this.log.debug('creating Query ', path, existingModifiers);
this.ref = ref;
this.modifiers = existingModifiers ? [...existingModifiers] : [];
}
setOrderBy(name: string, key?: string) {
if (key) {
this.modifiers.push(`${name}:${key}`);
} else {
this.modifiers.push(name);
}
}
setLimit(name: string, limit: number) {
this.modifiers.push(`${name}:${limit}`);
}
setFilter(name: string, value: any, key?:string) {
if (key) {
this.modifiers.push(`${name}:${value}:${typeof value}:${key}`);
} else {
this.modifiers.push(`${name}:${value}:${typeof value}`);
}
}
getModifiers(): Array<string> {
return [...this.modifiers];
}
getModifiersString(): string {
if (!this.modifiers || !Array.isArray(this.modifiers)) {
return '';
}
return this.modifiers.join('|');
}
}

View File

@ -0,0 +1,331 @@
/**
* @flow
*/
import { NativeModules } from 'react-native';
import Query from './query.js';
import Snapshot from './snapshot';
import Disconnect from './disconnect';
import { ReferenceBase } from './../base';
import { promisify, isFunction, isObject, tryJSONParse, tryJSONStringify, generatePushID } from './../../utils';
const FirebaseDatabase = NativeModules.FirebaseDatabase;
// https://firebase.google.com/docs/reference/js/firebase.database.Reference
/**
* @class Reference
*/
export default class Reference extends ReferenceBase {
db: FirebaseDatabase;
query: Query;
constructor(db: FirebaseDatabase, path: string, existingModifiers?: Array<string>) {
super(db.firebase, path);
this.db = db;
this.namespace = 'firebase:db:ref';
this.query = new Query(this, path, existingModifiers);
this.log.debug('Created new Reference', this.db._handle(path, existingModifiers));
}
/**
*
* @param bool
* @returns {*}
*/
keepSynced(bool: boolean) {
const path = this._dbPath();
return promisify('keepSynced', FirebaseDatabase)(path, bool);
}
/**
*
* @param value
* @returns {*}
*/
set(value: any) {
const path = this._dbPath();
const _value = this._serializeAnyType(value);
return promisify('set', FirebaseDatabase)(path, _value);
}
/**
*
* @param val
* @returns {*}
*/
update(val: Object) {
const path = this._dbPath();
const value = this._serializeObject(val);
return promisify('update', FirebaseDatabase)(path, value);
}
/**
*
* @returns {*}
*/
remove() {
return promisify('remove', FirebaseDatabase)(this._dbPath());
}
/**
*
* @param value
* @param onComplete
* @returns {*}
*/
push(value: any, onComplete: Function) {
if (value === null || value === undefined) {
const _path = this.path + '/' + generatePushID(this.db.serverTimeOffset);
return new Reference(this.db, _path);
}
const path = this._dbPath();
const _value = this._serializeAnyType(value);
return promisify('push', FirebaseDatabase)(path, _value)
.then(({ ref }) => {
const newRef = new Reference(this.db, ref);
if (isFunction(onComplete)) return onComplete(null, newRef);
return newRef;
}).catch((e) => {
if (isFunction(onComplete)) return onComplete(e, null);
return e;
});
}
on(eventName: string, cb: () => any, errorCb: () => any) {
if (!isFunction(cb)) throw new Error('The specified callback must be a function');
if (errorCb && !isFunction(errorCb)) throw new Error('The specified error callback must be a function');
const path = this._dbPath();
const modifiers = this.query.getModifiers();
const modifiersString = this.query.getModifiersString();
this.log.debug('adding reference.on', path, modifiersString, eventName);
return this.db.on(path, modifiersString, modifiers, eventName, cb, errorCb);
}
once(eventName: string = 'once', cb: (snapshot: Object) => void) {
const path = this._dbPath();
const modifiers = this.query.getModifiers();
const modifiersString = this.query.getModifiersString();
return promisify('onOnce', FirebaseDatabase)(path, modifiersString, modifiers, eventName)
.then(({ snapshot }) => new Snapshot(this, snapshot))
.then((snapshot) => {
if (isFunction(cb)) cb(snapshot);
return snapshot;
});
}
off(eventName?: string = '', origCB?: () => any) {
const path = this._dbPath();
const modifiersString = this.query.getModifiersString();
this.log.debug('ref.off(): ', path, modifiersString, eventName);
return this.db.off(path, modifiersString, eventName, origCB);
}
/**
* 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 {
const newRef = new Reference(this.db, this.path, this.query.getModifiers());
newRef.query.setOrderBy(name, key);
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 {
const newRef = new Reference(this.db, this.path, this.query.getModifiers());
newRef.query.setLimit(name, limit);
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 {
const newRef = new Reference(this.db, this.path, this.query.getModifiers());
newRef.query.setFilter(name, value, key);
return newRef;
}
onDisconnect() {
return new Disconnect(this.path);
}
child(path: string) {
return new Reference(this.db, this.path + '/' + path);
}
toString(): string {
return this._dbPath();
}
/**
* GETTERS
*/
/**
* Returns the parent ref of the current ref i.e. a ref of /foo/bar would return a new ref to '/foo'
* @returns {*}
*/
get parent(): Reference|null {
if (this.path === '/') return null;
return new Reference(this.db, this.path.substring(0, this.path.lastIndexOf('/')));
}
/**
* Returns a ref to the root of db - '/'
* @returns {Reference}
*/
get root(): Reference {
return new Reference(this.db, '/');
}
/**
* INTERNALS
*/
_dbPath(): string {
return this.path;
}
/**
*
* @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,
};
}
}

View File

@ -0,0 +1,86 @@
/**
* @flow
*/
import Reference from './reference.js';
import { isObject, deepGet, deepExists } from './../../utils';
export default class Snapshot {
static key: String;
static value: Object;
static exists: boolean;
static hasChildren: boolean;
static childKeys: String[];
ref: Object;
key: string;
value: any;
exists: boolean;
priority: any;
childKeys: Array<string>;
constructor(ref: Reference, snapshot: Object) {
this.ref = ref;
this.key = snapshot.key;
this.value = snapshot.value;
this.exists = snapshot.exists || true;
this.priority = snapshot.priority === undefined ? null : snapshot.priority;
this.childKeys = snapshot.childKeys || [];
}
/*
* DEFAULT API METHODS
*/
val() {
return this.value;
}
child(path: string) {
const value = deepGet(this.value, path);
const childRef = this.ref.child(path);
return new Snapshot(childRef, {
value,
key: childRef.key,
exists: value !== null,
childKeys: isObject(value) ? Object.keys(value) : [],
});
}
exists() {
return this.value !== null;
}
forEach(fn: (key: any) => any) {
return this.childKeys.forEach((key, i) => fn(this.child(key), i));
}
getPriority() {
return this.priority;
}
hasChild(path: string) {
return deepExists(this.value, path);
}
hasChildren() {
return this.numChildren() > 0;
}
numChildren() {
if (!isObject(this.value)) return 0;
return Object.keys(this.value).length;
}
/*
* EXTRA API METHODS
*/
map(fn: (key: string) => mixed) {
const arr = [];
this.forEach((item, i) => arr.push(fn(item, i)));
return arr;
}
reverseMap(fn: (key: string) => mixed) {
return this.map(fn).reverse();
}
}