@react-native-firebase/database
Version:
React Native Firebase - The Firebase Realtime Database is a cloud-hosted database. Data is stored as JSON and synchronized in realtime to every connected client. React Native Firebase provides native integration with the Android & iOS Firebase SDKs, suppo
549 lines (467 loc) • 16.8 kB
JavaScript
/*
* Copyright (c) 2016-present Invertase Limited & Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this library except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import {
isBoolean,
isFunction,
isNull,
isNumber,
isObject,
isString,
isUndefined,
pathIsEmpty,
pathToUrlEncodedString,
ReferenceBase,
} from '@react-native-firebase/app/lib/common';
import DatabaseDataSnapshot from './DatabaseDataSnapshot';
import DatabaseSyncTree from './DatabaseSyncTree';
const eventTypes = ['value', 'child_added', 'child_changed', 'child_moved', 'child_removed'];
// To avoid React Native require cycle warnings
let DatabaseReference = null;
export function provideReferenceClass(databaseReference) {
DatabaseReference = databaseReference;
}
// Internal listener count
let listeners = 0;
export default class DatabaseQuery extends ReferenceBase {
constructor(database, path, modifiers) {
super(path);
this._database = database;
this._modifiers = modifiers;
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.Query.html#endat
*/
get ref() {
return new DatabaseReference(this._database, this.path);
}
/**
*
* @param value
* @param key
* @return {DatabaseQuery}
*/
endAt(value, key) {
if (!isNumber(value) && !isString(value) && !isBoolean(value) && !isNull(value)) {
throw new Error(
"firebase.database().ref().endAt(*) 'value' must be a number, string, boolean or null value.",
);
}
if (!isUndefined(key) && !isString(key)) {
throw new Error(
"firebase.database().ref().endAt(_, *) 'key' must be a string value if defined.",
);
}
if (this._modifiers.hasEndAt()) {
throw new Error(
'firebase.database().ref().endAt() Ending point was already set (by another call to endAt or equalTo).',
);
}
const modifiers = this._modifiers._copy().endAt(value, key);
modifiers.validateModifiers('firebase.database().ref().endAt()');
return new DatabaseQuery(this._database, this.path, modifiers);
}
/**
*
* @param value
* @param key
* @return {DatabaseQuery}
*/
equalTo(value, key) {
if (!isNumber(value) && !isString(value) && !isBoolean(value) && !isNull(value)) {
throw new Error(
"firebase.database().ref().equalTo(*) 'value' must be a number, string, boolean or null value.",
);
}
if (!isUndefined(key) && !isString(key)) {
throw new Error(
"firebase.database().ref().equalTo(_, *) 'key' must be a string value if defined.",
);
}
if (this._modifiers.hasStartAt()) {
throw new Error(
'firebase.database().ref().equalTo() Starting point was already set (by another call to startAt or equalTo).',
);
}
if (this._modifiers.hasEndAt()) {
throw new Error(
'firebase.database().ref().equalTo() Ending point was already set (by another call to endAt or equalTo).',
);
}
return this.startAt(value, key).endAt(value, key);
}
/**
*
* @param other
* @return {boolean}
*/
isEqual(other) {
if (!(other instanceof DatabaseQuery)) {
throw new Error("firebase.database().ref().isEqual(*) 'other' must be an instance of Query.");
}
const sameApp = other._database.app === this._database.app;
const sameDatabasePath = other.toString() === this.toString();
const sameModifiers = other._modifiers.toString() === this._modifiers.toString();
return sameApp && sameDatabasePath && sameModifiers;
}
/**
*
* @param limit
* @return {DatabaseQuery}
*/
limitToFirst(limit) {
if (this._modifiers.isValidLimit(limit)) {
throw new Error(
"firebase.database().ref().limitToFirst(*) 'limit' must be a positive integer value.",
);
}
if (this._modifiers.hasLimit()) {
throw new Error(
'firebase.database().ref().limitToFirst(*) Limit was already set (by another call to limitToFirst, or limitToLast)',
);
}
return new DatabaseQuery(
this._database,
this.path,
this._modifiers._copy().limitToFirst(limit),
);
}
/**
*
* @param limit
* @return {DatabaseQuery}
*/
limitToLast(limit) {
if (this._modifiers.isValidLimit(limit)) {
throw new Error(
"firebase.database().ref().limitToLast(*) 'limit' must be a positive integer value.",
);
}
if (this._modifiers.hasLimit()) {
throw new Error(
'firebase.database().ref().limitToLast(*) Limit was already set (by another call to limitToFirst, or limitToLast)',
);
}
return new DatabaseQuery(this._database, this.path, this._modifiers._copy().limitToLast(limit));
}
/**
*
* @param eventType
* @param callback
* @param context
* @return {DatabaseQuery}
*/
off(eventType, callback, context) {
//
if (arguments.length === 0) {
// Firebase Docs:
// if no eventType or callback is specified, all callbacks for the Reference will be removed
return DatabaseSyncTree.removeListenersForRegistrations(
DatabaseSyncTree.getRegistrationsByPath(this.path),
);
}
if (!isUndefined(eventType) && !eventTypes.includes(eventType)) {
throw new Error(
`firebase.database().ref().off(*) 'eventType' must be one of ${eventTypes.join(', ')}.`,
);
}
if (!isUndefined(callback) && !isFunction(callback)) {
throw new Error("firebase.database().ref().off(_, *) 'callback' must be a function.");
}
if (!isUndefined(context) && !isObject(context)) {
throw new Error("firebase.database().ref().off(_, _, *) 'context' must be an object.");
}
// 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 && callback) {
const registration = DatabaseSyncTree.getOneByPathEventListener(
this.path,
eventType,
callback,
);
if (!registration) {
return [];
}
// remove the paired cancellation registration if any exist
DatabaseSyncTree.removeListenersForRegistrations([`${registration}$cancelled`]);
// remove only the first registration to match firebase web sdk
// call multiple times to remove multiple registrations
return DatabaseSyncTree.removeListenerRegistrations(callback, [registration]);
}
// Firebase Docs:
// If a callback is not specified, all callbacks for the specified eventType will be removed.
const registrations = DatabaseSyncTree.getRegistrationsByPathEvent(this.path, eventType);
DatabaseSyncTree.removeListenersForRegistrations(
DatabaseSyncTree.getRegistrationsByPathEvent(this.path, `${eventType}$cancelled`),
);
return DatabaseSyncTree.removeListenersForRegistrations(registrations);
}
/**
*
* @param eventType
* @param callback
* @param cancelCallbackOrContext
* @param context
* @return {DatabaseQuery}
*/
on(eventType, callback, cancelCallbackOrContext, context) {
if (!eventTypes.includes(eventType)) {
throw new Error(
`firebase.database().ref().on(*) 'eventType' must be one of ${eventTypes.join(', ')}.`,
);
}
if (!isFunction(callback)) {
throw new Error("firebase.database().ref().on(_, *) 'callback' must be a function.");
}
if (
!isUndefined(cancelCallbackOrContext) &&
!isFunction(cancelCallbackOrContext) &&
!isObject(cancelCallbackOrContext)
) {
throw new Error(
"firebase.database().ref().on(_, _, *) 'cancelCallbackOrContext' must be a function or object.",
);
}
if (!isUndefined(context) && !isObject(context)) {
throw new Error("firebase.database().ref().on(_, _, _, *) 'context' must be an object.");
}
const queryKey = this._generateQueryKey();
const eventRegistrationKey = this._generateQueryEventKey(eventType);
const registrationCancellationKey = `${eventRegistrationKey}$cancelled`;
const _context =
cancelCallbackOrContext && !isFunction(cancelCallbackOrContext)
? cancelCallbackOrContext
: context;
// Add a new SyncTree registration
DatabaseSyncTree.addRegistration({
eventType,
ref: this.ref,
path: this.path,
key: queryKey,
appName: this._database.app.name,
dbURL: this._database._customUrlOrRegion,
eventRegistrationKey,
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
DatabaseSyncTree.addRegistration({
ref: this.ref,
once: true,
path: this.path,
key: queryKey,
appName: this._database.app.name,
dbURL: this._database._customUrlOrRegion,
eventType: `${eventType}$cancelled`,
eventRegistrationKey: registrationCancellationKey,
listener: _context ? cancelCallbackOrContext.bind(_context) : cancelCallbackOrContext,
});
}
this._database.native.on({
eventType,
path: this.path,
key: queryKey,
appName: this._database.app.name,
modifiers: this._modifiers.toArray(),
hasCancellationCallback: isFunction(cancelCallbackOrContext),
registration: {
eventRegistrationKey,
key: queryKey,
registrationCancellationKey,
},
});
// increment number of listeners - just a short way of making
// every registration unique per .on() call
listeners += 1;
return callback;
}
/**
* @param eventType
* @param successCallBack
* @param failureCallbackOrContext
* @param context
*/
once(eventType, successCallBack, failureCallbackOrContext, context) {
if (!eventTypes.includes(eventType)) {
throw new Error(
`firebase.database().ref().once(*) 'eventType' must be one of ${eventTypes.join(', ')}.`,
);
}
if (!isUndefined(successCallBack) && !isFunction(successCallBack)) {
throw new Error("firebase.database().ref().once(_, *) 'successCallBack' must be a function.");
}
if (
!isUndefined(failureCallbackOrContext) &&
!isObject(failureCallbackOrContext) &&
!isFunction(failureCallbackOrContext)
) {
throw new Error(
"firebase.database().ref().once(_, _, *) 'failureCallbackOrContext' must be a function or context.",
);
}
if (!isUndefined(context) && !isObject(context)) {
throw new Error(
"firebase.database().ref().once(_, _, _, *) 'context' must be a context object.",
);
}
const modifiers = this._modifiers._copy().toArray();
return this._database.native
.once(this.path, modifiers, eventType)
.then(result => {
let dataSnapshot;
let previousChildName;
// Child based events return a previousChildName
if (eventType === 'value') {
dataSnapshot = new DatabaseDataSnapshot(this.ref, result);
} else {
dataSnapshot = new DatabaseDataSnapshot(this.ref, result.snapshot);
previousChildName = result.previousChildName;
}
if (isFunction(successCallBack)) {
if (isObject(failureCallbackOrContext)) {
successCallBack.bind(failureCallbackOrContext)(dataSnapshot, previousChildName);
} else if (isObject(context)) {
successCallBack.bind(context)(dataSnapshot, previousChildName);
} else {
successCallBack(dataSnapshot, previousChildName);
}
}
return dataSnapshot;
})
.catch(error => {
if (isFunction(failureCallbackOrContext)) {
failureCallbackOrContext(error);
}
return Promise.reject(error);
});
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.Query.html#orderbychild
*/
orderByChild(path) {
if (!isString(path)) {
throw new Error("firebase.database().ref().orderByChild(*) 'path' must be a string value.");
}
if (pathIsEmpty(path)) {
throw new Error(
"firebase.database().ref().orderByChild(*) 'path' cannot be empty. Use orderByValue instead.",
);
}
if (this._modifiers.hasOrderBy()) {
throw new Error(
"firebase.database().ref().orderByChild(*) You can't combine multiple orderBy calls.",
);
}
const modifiers = this._modifiers._copy().orderByChild(path);
modifiers.validateModifiers('firebase.database().ref().orderByChild()');
return new DatabaseQuery(this._database, this.path, modifiers);
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.Query.html#orderbykey
*/
orderByKey() {
if (this._modifiers.hasOrderBy()) {
throw new Error(
"firebase.database().ref().orderByKey() You can't combine multiple orderBy calls.",
);
}
const modifiers = this._modifiers._copy().orderByKey();
modifiers.validateModifiers('firebase.database().ref().orderByKey()');
return new DatabaseQuery(this._database, this.path, modifiers);
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.Query.html#orderbypriority
*/
orderByPriority() {
if (this._modifiers.hasOrderBy()) {
throw new Error(
"firebase.database().ref().orderByPriority() You can't combine multiple orderBy calls.",
);
}
const modifiers = this._modifiers._copy().orderByPriority();
modifiers.validateModifiers('firebase.database().ref().orderByPriority()');
return new DatabaseQuery(this._database, this.path, modifiers);
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.Query.html#orderbyvalue
*/
orderByValue() {
if (this._modifiers.hasOrderBy()) {
throw new Error(
"firebase.database().ref().orderByValue() You can't combine multiple orderBy calls.",
);
}
const modifiers = this._modifiers._copy().orderByValue();
modifiers.validateModifiers('firebase.database().ref().orderByValue()');
return new DatabaseQuery(this._database, this.path, modifiers);
}
startAt(value, key) {
if (!isNumber(value) && !isString(value) && !isBoolean(value) && !isNull(value)) {
throw new Error(
"firebase.database().ref().startAt(*) 'value' must be a number, string, boolean or null value.",
);
}
if (!isUndefined(key) && !isString(key)) {
throw new Error(
"firebase.database().ref().startAt(_, *) 'key' must be a string value if defined.",
);
}
if (this._modifiers.hasStartAt()) {
throw new Error(
'firebase.database().ref().startAt() Starting point was already set (by another call to startAt or equalTo).',
);
}
const modifiers = this._modifiers._copy().startAt(value, key);
modifiers.validateModifiers('firebase.database().ref().startAt()');
return new DatabaseQuery(this._database, this.path, modifiers);
}
toJSON() {
return this.toString();
}
toString() {
return `${this._database._customUrlOrRegion}${pathToUrlEncodedString(this.path)}`;
}
keepSynced(bool) {
if (!isBoolean(bool)) {
throw new Error(
"firebase.database().ref().keepSynced(*) 'bool' value must be a boolean value.",
);
}
return this._database.native.keepSynced(
this._generateQueryKey(),
this.path,
this._modifiers.toArray(),
bool,
);
}
// Generates a unique string for a query
// Ensures any queries called in various orders keep the same key
_generateQueryKey() {
return `$${this._database._customUrlOrRegion}$/${this.path}$${
this._database.app.name
}$${this._modifiers.toString()}`;
}
// Generates a unique event registration key
_generateQueryEventKey(eventType) {
return `${this._generateQueryKey()}$${listeners}$${eventType}`;
}
}