const util = require("./util.js");
const Listener = require('./listener.js');
const objNotExistErr = "Object with key does not exist";
const objExistErr = "Object with key already exists";
const transAbortErr = "transaction not committed!";
let _getUnixTS = () => Math.floor(new Date().getTime() / 1000);
let _getSnapshot = async(ref, key) => {
if (key) ref = ref.child(key);
return await ref.once("value");
}
let _getSnapshotByQuery = async(ref, field, value) => {
// TODO: bug with equalTo found
// return await ref.orderByChild(field).equalTo(value).once("value");
return await ref.once("value");
}
let _getSnapshotByBound = async(ref, field, bound) => {
// TODO: possible bug with startAt and endAt
// return await ref.orderByChild(field).startAt(bound[0])
// .endAt(bound[1]).once("value");
return await ref.once("value");
}
let _remove = async(ref, key) => await ref.child(key).remove()
let _update = async(ref, key, fieldToVal) => {
return await ref.child(key).update(fieldToVal);
}
let _set = async(ref, value) => await ref.set(value);
let _transaction = async(ref, key, field, atomicFn) => {
return await new Promise((resolve, reject) => {
ref.child(key).child(field).transaction(atomicFn, (err, commit, snapshot) => {
if (err) reject(err);
else if (!commit) reject(new Error(transAbortErr));
else resolve(FirebaseObject.getByKey(ref, key));
}, false);
});
}
let _multipleConstructCb = (ref) => {
return snapshot => {
if (!snapshot.exists()) return [];
var result = [];
snapshot.forEach(childSnapshot => {
result.push(new FirebaseObject(ref, childSnapshot));
});
return result;
}
}
let _listenOnRef = (ref, cb, isChild, once) => {
(new Listener(ref, isChild, cb, once)).listen();
}
/**
* Generic FirebaseObject class (extended by all mapped classes)
*/
class FirebaseObject {
/**
* Create a FirebaseObject
* @constructor
* @param {object} ref - The database reference.
* @param {object} snapshot - The snapshot of data from vanilla firebase db admin sdk.
*/
constructor(ref, snapshot) {
this._ref = ref;
this._event = "value";
this._synced = true;
if (!snapshot) {
this._value = null;
this._key = null;
} else {
this._value = snapshot.val();
this._key = snapshot.key;
}
}
/**
* JSON representation of FirebaseObject
* @typedef {Object} JSON
* @property {string} key - The key of the object
* @property {object} value - The value of the object
*/
/**
* Package object into easy digestable json
* @returns {JSON}
*/
json() {
return {
key: this._key,
value: this._value
}
}
/**
* Package objects into easy digestable jsons
* @returns {Array<JSON>}
*/
static jsonAll(objs) {
return objs.map(obj => obj.json());
}
static _copyValues(src, dest) {
dest._value = src._value;
dest._synced = true;
}
/**
* toString method for debugging purposes
* @returns {string}
*/
toString() {
return util.toString("FirebaseObject", this);
}
get[Symbol.toStringTag]() {
return "FirebaseObject";
}
/**
* Possible event strings: 'value', 'added', 'changed', 'removed'.
* 'value' indicates its a one time value fetched objects.
* 'added' indicates the object was fetched in child_added listener.
* 'changed' indicates the object was fetched in child_changed listener.
* 'removed' indiciates the object was fetched in child_removed listener.
* @returns {string}
*/
getEvent() {
return this._event;
}
/**
* Returns value of the object.
* @returns {object}
*/
getValue() {
return this._value;
}
/**
* Returns epoch unix timestamp of when object was last changed, or null if no update field set.
* @returns {null|number}
*/
getTimeUpdated() {
if (!this._value) return null;
return this._value._updated;
}
/**
* Returns key of the object.
* @returns {string}
*/
getKey() {
return this._key;
}
/**
* Returns whether or not object is synced with database.
* @returns {boolean}
*/
isSynced() {
return this._synced;
}
/**
* Pushes changes to this object to database
* @async
*/
async push() {
await this.update(this._value)
this._synced = true;
}
/**
* Fetches changes to this object from database
* @async
*/
async fetch() {
let obj = await FirebaseObject.getByKey(this._ref, this._key);
FirebaseObject._copyValues(obj, this);
}
/**
* Deletes object locally and remotely
* @async
*/
async delete() {
let obj = await FirebaseObject.deleteByKey(this._ref, this._key);
this._value = null;
this._synced = true;
}
/**
* Updates object locally and remotely
* @async
* @param {object} fieldToVal - object with fields of the value you want to update
*/
async update(fieldToVal) {
let obj = await FirebaseObject.updateByKey(this._ref, this._key, fieldToVal);
FirebaseObject._copyValues(obj, this);
}
/**
* Initializes listener for all database event types (except 'value')
* @variation 1
* @param {string} field - specific field you want to listen for
* @param {function} emitCb - callback that triggers when changes detected
* @param {boolean} [once] - set to true if you want event to only fire once
*/
listenForChanges(field, emitCb, once) {
let that = this;
_listenOnRef(this._ref.child(this._key), type => {
return async snapshot => {
if (!field || snapshot.key == field) {
let obj = await FirebaseObject.getByKey(that._ref, that._key);
obj._event = type;
emitCb(obj);
return true;
}
return false;
}
}, field != null, once);
}
/**
* Initializes listener for all database event types (except 'value')
* @variation 2
* @param {function} emitCb - callback that triggers when changes detected
* @param {boolean} [once] - set to true if you want event to only fire once
*/
listenForChanges(emitCb, once) {
this.listenForChanges(null, emitCb, once);
}
/**
* Run transaction on field of object with atomic function (applies changes locally and remotely)
* @async
* @param {string} field - field you wish to commit the transaction on
* @param {function} atomicFn - function that represents the transaction being done
*/
async transaction(field, atomicFn) {
let obj = await FirebaseObject
.transaction(this._ref, this._key, field, atomicFn)
FirebaseObject._copyValues(obj, this);
}
/**
* Run transaction (increase/decrease of number) on field of object (applies changes locally and remotely)
* @async
* @param {string} field - field you wish to commit the transaction on
* @param {number} delta - amount to change the number by
*/
async transactNum(field, delta) {
let obj = await FirebaseObject
.transactNum(this._ref, this._key, field, delta)
FirebaseObject._copyValues(obj, this);
}
/**
* Run transaction (append item to list) on field of object (applies changes locally and remotely)
* @async
* @param {string} field - field you wish to commit the transaction on
* @param value - value you want to append to the array
* @param {boolean} [isUniqueList] - True means its a Set, False means its a List
*/
async transactAppendToList(field, value, isUniqueList) {
let obj = await FirebaseObject
.transactAppendToList(this._ref, this._key, field, value, isUniqueList)
FirebaseObject._copyValues(obj, this);
}
/**
* Run transaction (remove item from list) on field of object (applies changes locally and remotely)
* @async
* @param {string} field - field you wish to commit the transaction on
* @param value - value you want to remove from the array
* @param {boolean} [isUniqueList] - True means its a Set, False means its a List
*/
async transactRemoveFromList(field, value, isUniqueList) {
let obj = await FirebaseObject
.transactRemoveFromList(this._ref, this._key, field, value, isUniqueList)
FirebaseObject._copyValues(obj, this);
}
/**
* Check if object exists or not
* @async
* @param {object} ref - database reference
* @param {string} key - key of the object
* @returns {Promise<boolean>}
*/
static async exists(ref, key) {
let snapshot = await _getSnapshot(ref, key);
return snapshot.exists();
}
/**
* Check if all objects exist or not
* @async
* @param {object} ref - database reference
* @param {Array<string>} keys - key of the object
* @returns {Promise<boolean>}
*/
static async allExists(ref, keys) {
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
let b = await FirebaseObject.exists(ref, key);
if (!b) return false;
}
return true;
}
/**
* Get object by database reference and key
* @async
* @param {object} ref - database reference
* @param {string} key - key of the object
* @returns {Promise<FirebaseObject>} - throws error if key does not exist
*/
static async getByKey(ref, key) {
let snapshot = await _getSnapshot(ref, key);
if (!snapshot.exists())
throw new Error(objNotExistErr);
return new FirebaseObject(ref, snapshot);
}
/**
* Get all objects by database reference
* @async
* @param {object} ref - database reference
* @returns {Promise<Array<FirebaseObject>>}
*/
static async getAll(ref) {
let snapshot = await _getSnapshot(ref);
return _multipleConstructCb(ref)(snapshot);
}
/**
* Get all objects by database reference and keys
* @async
* @param {object} ref - database reference
* @param {Array<string>} keys - keys of objects
* @returns {Promise<Array<FirebaseObject>>}
*/
static async getAllByKeys(ref, keys) {
let objs = await FirebaseObject.getAll(ref);
return objs.filter(obj => keys.indexOf(obj._key) >= 0);
}
/**
* Get all objects by database reference and values
* @async
* @param {object} ref - database reference
* @param {object} fieldToVal - field of the object mapped to value of that field
* @returns {Promise<Array<FirebaseObject>>}
*/
static async getAllByFields(ref, fieldToVal) {
let primaryField = Object.keys(fieldToVal)[0];
let primaryVal = fieldToVal[primaryField];
let snapshot = await _getSnapshotByQuery(ref, primaryField, primaryVal);
let objects = _multipleConstructCb(ref)(snapshot);
return objects.filter(object => {
return Object.keys(fieldToVal).reduce((bool, key) => {
let val = fieldToVal[key];
let objVal = object._value[key];
return bool && objVal == val;
}, true);
});
}
/**
* Get all objects by database reference and bounds
* @async
* @param {object} ref - database reference
* @param {object} fieldToBound - field of the object mapped to bound of that field (bound is an array with 2 items: start and end (both exclusivley))
* @returns {Promise<Array<FirebaseObject>>}
*/
static async getAllByBounds(ref, fieldToBound) {
let primaryField = Object.keys(fieldToBound)[0];
let primaryBound = fieldToBound[primaryField];
let snapshot = await _getSnapshotByBound(ref, primaryField, primaryBound);
let objects = _multipleConstructCb(ref)(snapshot);
return objects.filter(object => {
return Object.keys(fieldToBound).reduce((bool, key) => {
let bound = fieldToBound[key];
let objectVal = object._value[key];
if (objectVal < bound[0] || objectVal > bound[1])
return false;
return true;
}, true);
});
}
/**
* Get all objects by database reference and field that starts with given value
* @async
* @param {object} ref - database reference
* @param {string} field - field of the object
* @param {string} value - the thing the field starts with
* @returns {Promise<Array<FirebaseObject>>}
*/
static async getAllThatStartsWith(ref, field, value) {
let bound = [value, value + "\uf8ff"];
let snapshot = await _getSnapshotByBound(ref, field, bound);
return _multipleConstructCb(ref)(snapshot);
}
/**
* Delete object by reference and key
* @async
* @param {object} ref - database reference
* @param {string} key - key of the object
* @returns {Promise<FirebaseObject>} - throws error if key does not exist
*/
static async deleteByKey(ref, key) {
let obj = await FirebaseObject.getByKey(ref, key);
await _remove(ref, key);
return obj;
}
/**
* Update object by reference, key, and values
* @async
* @param {object} ref - database reference
* @param {string} key - key of the object
* @param {object} fieldToVal - object with fields of the value you want to update
* @returns {Promise<FirebaseObject>} - throws error if key does not exist
*/
static async updateByKey(ref, key, fieldToVal) {
let exists = await FirebaseObject.exists(ref, key);
if (!exists) throw new Error(objNotExistErr);
fieldToVal._updated = _getUnixTS();
await _update(ref, key, fieldToVal);
return await FirebaseObject.getByKey(ref, key);
}
/**
* Create object by reference and with given value (assigns automatic key)
* @async
* @param {object} ref - database reference
* @param {object} fieldToVal - field of the object mapped to value of that field
* @returns {Promise<FirebaseObject>}
*/
static async createByAutoKey(ref, fieldToVal) {
fieldToVal._updated = _getUnixTS();
let newRef = ref.push();
await _set(newRef, fieldToVal);
return await FirebaseObject.getByKey(ref, newRef.key);
}
/**
* Create object by reference and with given value (assigns manual key)
* @async
* @param {object} ref - database reference
* @param {string} key - key of object
* @param {object} fieldToVal - field of the object mapped to value of that field
* @returns {Promise<FirebaseObject>} - throws error if key is taken
*/
static async createByManualKey(ref, key, fieldToVal) {
let exists = await FirebaseObject.exists(ref, key);
if (exists) throw new Error(objExistErr);
fieldToVal._updated = _getUnixTS();
await _set(ref.child(key), fieldToVal);
return await FirebaseObject.getByKey(ref, key);
}
/**
* Run transaction on field of object with atomic function
* @async
* @param {object} ref - database reference
* @param {string} key - key of object
* @param {string} field - field you wish to commit the transaction on
* @param {function} atomicFn - function that represents the transaction being done
* @returns {Promise<FirebaseObject>}
*/
static async transaction(ref, key, field, atomicFn) {
return await _transaction(ref, key, field, atomicFn);
}
/**
* Run transaction (increase/decrease of number) on field of object
* @async
* @param {object} ref - database reference
* @param {string} key - key of object
* @param {string} field - field you wish to commit the transaction on
* @param {number} delta - amount to change the number by
* @returns {Promise<FirebaseObject>}
*/
static async transactNum(ref, key, field, delta) {
return await FirebaseObject.transaction(ref, key, field, (value) => {
value = value || 0;
value += delta;
return value;
});
}
/**
* Run transaction (append item to list) on field of object
* @async
* @param {object} ref - database reference
* @param {string} key - key of object
* @param {string} field - field you wish to commit the transaction on
* @param value - value you want to append to the array
* @param {boolean} [isUniqueList] - True means its a Set, False means its a List
* @returns {Promise<FirebaseObject>}
*/
static async transactAppendToList(ref, key, field, value, isUniqueList) {
return await FirebaseObject.transaction(ref, key, field, (arr) => {
if (isUniqueList) {
arr = arr || new Set();
arr.add(value);
arr = Array.from(arr);
} else {
arr = arr || [];
arr.push(value);
}
return arr;
});
}
/**
* Run transaction (remove item from list) on field of object
* @async
* @param {object} ref - database reference
* @param {string} key - key of object
* @param {string} field - field you wish to commit the transaction on
* @param value - value you want to remove from the array
* @param {boolean} [isUniqueList] - True means its a Set, False means its a List
* @returns {Promise<FirebaseObject>}
*/
static async transactRemoveFromList(ref, key, field, value, isUniqueList) {
return await FirebaseObject.transaction(ref, key, field, (arr) => {
if (isUniqueList) {
arr = arr || new Set();
arr.delete(value);
arr = Array.from(arr);
} else {
arr = arr || [];
var index = arr.indexOf(value);
arr.splice(index, 1);
}
return arr;
});
}
/**
* Initializes listener for all database event types (except 'value') with query
* @param {object} ref - database reference
* @param {string} [field] - specific field you want to listen for (needed if value passed in)
* @param [value] - value the field should be equal to (needed if field passed in)
* @param {function} emitCb - callback that triggers when changes detected
* @param {boolean} [once] - set to true if you want event to only fire once
*/
static listenForQuery(ref, field, value, emitCb, once) {
_listenOnRef(ref, type => {
return snapshot => {
var obj = new FirebaseObject(ref, snapshot);
obj._event = type;
if (!field || !value || obj._value[field] == value) {
obj._key = snapshot.key;
obj._event = type;
emitCb(obj);
return true;
}
return false;
}
}, false, once);
}
}
// EXPORTS
module.exports = FirebaseObject;