[v6] Implement Realtime Database (#2195)

* [database] recreate database branch based off of #2185

* [database] cleanup linting issues

* [database] enable tests

* [database] add to tests deps
This commit is contained in:
Mike Diarmid
2019-06-04 15:25:35 +01:00
committed by Elliot Hesp
parent da9c32000d
commit fe959bb842
126 changed files with 12045 additions and 46 deletions

View File

@@ -0,0 +1,198 @@
/* eslint-disable no-console */
/*
* 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 { isString, isArray, isFunction, isObject } from '@react-native-firebase/common';
import { deepGet } from '@react-native-firebase/common/lib/deeps';
export default class DatabaseDataSnapshot {
constructor(reference, snapshot) {
this._snapshot = snapshot;
if (reference.key !== snapshot.key) {
// reference is a query?
this._ref = reference.ref.child(snapshot.key);
} else {
this._ref = reference;
}
// TODO #894
// if (this._ref.path === '.info/connected') {
// Handle 1/0 vs true/false values on ios/android
// }
}
get key() {
return this._snapshot.key;
}
get ref() {
return this._ref;
}
/**
* Returns a new snapshot of the child location
* @param path
* @returns {DatabaseDataSnapshot}
*/
child(path) {
if (!isString(path)) {
throw new Error(`snapshot().child(*) 'path' must be a string value`);
}
let value = deepGet(this._snapshot.value, path);
if (value === undefined) value = null;
const childRef = this._ref.child(path);
return new DatabaseDataSnapshot(childRef, {
value,
key: childRef.key,
exists: value !== null,
childKeys: isObject(value) ? Object.keys(value) : [],
});
}
/**
* Returns whether the value exists
*
* @returns {(function())|((path: PathLike, callback: (exists: boolean) => void) => void)|boolean|exists|(() => boolean)}
*/
exists() {
return this._snapshot.exists;
}
/**
* Exports value and priority
*
* @returns {{'.priority': *, '.value': *}}
*/
exportVal() {
let { value } = this._snapshot;
if (isObject(value) || isArray(value)) {
value = JSON.parse(JSON.stringify(value));
}
return {
'.value': value,
'.priority': this._snapshot.priority,
};
}
/**
* Iterate over keys in order from Firebase
*
* @param action
* @return {boolean}
*/
forEach(action) {
if (!isFunction(action)) {
throw new Error(`snapshot.forEach(*) 'action' must be a function.`);
}
// If the value is an array,
if (isArray(this._snapshot.value)) {
return this._snapshot.value.some((value, i) => {
const snapshot = this.child(i.toString());
return action(snapshot, i) === true;
});
}
if (!this._snapshot.childKeys.length) {
return false;
}
let cancelled = false;
for (let i = 0; i < this._snapshot.childKeys.length; i++) {
const key = this._snapshot.childKeys[i];
const snapshot = this.child(key);
const actionReturn = action(snapshot, i);
if (actionReturn === true) {
cancelled = true;
break;
}
}
return cancelled;
}
getPriority() {
return this._snapshot.priority;
}
/**
* Checks the returned value for a nested child path
*
* @param path
* @returns {boolean}
*/
hasChild(path) {
if (!isString(path)) {
throw new Error(`snapshot.hasChild(*) 'path' must be a string value.`);
}
return deepGet(this._snapshot.value, path) !== undefined;
}
/**
* Returns whether the snapshot has any children
*
* @returns {boolean}
*/
hasChildren() {
return this.numChildren() > 0;
}
/**
* Returns the number of children this snapshot has
*
* @returns {number}
*/
numChildren() {
const { value } = this._snapshot;
if (isArray(value)) return value.length;
if (!isObject(value)) return 0;
return Object.keys(value).length;
}
/**
* Overrides the .toJSON implementation for snapshot
* Same as snapshot.val()
* @returns {any}
*/
toJSON() {
return this.val();
}
/**
* Returns the serialized value of the snapshot returned from Firebase
*
* @returns {any}
*/
val() {
const { value } = this._snapshot;
if (isObject(value) || isArray(value)) {
return JSON.parse(JSON.stringify(value));
}
return value;
}
}

View File

@@ -0,0 +1,141 @@
/* eslint-disable no-console */
/*
* 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 {
isFunction,
isNull,
isNumber,
isObject,
isString,
isUndefined,
promiseWithOptionalCallback,
} from '@react-native-firebase/common';
export default class DatabaseOnDisconnect {
constructor(reference) {
this._ref = reference;
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.OnDisconnect#cancel
*/
cancel(onComplete) {
if (!isUndefined(onComplete) && !isFunction(onComplete)) {
throw new Error(
`firebase.database().ref().onDisconnect().cancel(*) 'onComplete' must be a function if provided.`,
);
}
return promiseWithOptionalCallback(
this._ref._database.native.onDisconnectCancel(this._ref.path),
onComplete,
);
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.OnDisconnect#remove
*/
remove(onComplete) {
if (!isUndefined(onComplete) && !isFunction(onComplete)) {
throw new Error(
`firebase.database().ref().onDisconnect().remove(*) 'onComplete' must be a function if provided.`,
);
}
return promiseWithOptionalCallback(
this._ref._database.native.onDisconnectRemove(this._ref.path),
onComplete,
);
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.OnDisconnect#set
*/
set(value, onComplete) {
if (isUndefined(value)) {
throw new Error(`firebase.database().ref().value(*) 'value' must be defined.`);
}
if (!isUndefined(onComplete) && !isFunction(onComplete)) {
throw new Error(
`firebase.database().ref().onDisconnect().set(_, *) 'onComplete' must be a function if provided.`,
);
}
return promiseWithOptionalCallback(
this._ref._database.native.onDisconnectSet(this._ref.path, { value }),
onComplete,
);
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.OnDisconnect#setwithpriority
*/
setWithPriority(value, priority, onComplete) {
if (isUndefined(value)) {
throw new Error(`firebase.database().ref().setWithPriority(*) 'value' must be defined.`);
}
if (!isNumber(priority) && !isString(priority) && !isNull(priority)) {
throw new Error(
`firebase.database().ref().onDisconnect().setWithPriority(_, *) 'priority' must be a number, string or null value.`,
);
}
if (!isUndefined(onComplete) && !isFunction(onComplete)) {
throw new Error(
`firebase.database().ref().onDisconnect().setWithPriority(_, _, *) 'onComplete' must be a function if provided.`,
);
}
return promiseWithOptionalCallback(
this._ref._database.native.onDisconnectSetWithPriority(this._ref.path, { value, priority }),
onComplete,
);
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.OnDisconnect#update
*/
update(values, onComplete) {
if (!isObject(values)) {
throw new Error(
`firebase.database().ref().onDisconnect().update(*) 'values' must be an object.`,
);
}
if (!Object.keys(values).length) {
throw new Error(
`firebase.database().ref().onDisconnect().update(*) 'values' must be an object containing multiple values.`,
);
}
// TODO validate keys; / . # $ |
if (!isUndefined(onComplete) && !isFunction(onComplete)) {
throw new Error(
`firebase.database().ref().onDisconnect().update(_, *) 'onComplete' must be a function if provided.`,
);
}
return promiseWithOptionalCallback(
this._ref._database.native.onDisconnectUpdate(this._ref.path, { values }),
onComplete,
);
}
}

View File

@@ -0,0 +1,539 @@
/* eslint-disable no-console */
/*
* 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/common';
import DatabaseReference from './DatabaseReference';
import DatabaseDataSnapshot from './DatabaseDataSnapshot';
import DatabaseSyncTree from './DatabaseSyncTree';
const eventTypes = ['value', 'child_added', 'child_changed', 'child_moved', 'child_removed'];
// 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() {
// TODO require cycle warning?
// Require cycle: ../packages/database/lib/DatabaseReference.js -> ../packages/database/lib/DatabaseQuery.js -> ../packages/database/lib/DatabaseReference.js
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.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.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.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, // Not 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,
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,
});
}
// TODO appName, DB already passed along?
// TODO what needs going into the object?
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.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);
// eslint-disable-next-line prefer-destructuring
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.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.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.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.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.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}`;
}
}

View File

@@ -0,0 +1,237 @@
/* eslint-disable no-console */
/*
* 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 { isNull, isNumber, isString } from '@react-native-firebase/common';
const CONSTANTS = {
VIEW_FROM_LEFT: 'left',
VIEW_FROM_RIGHT: 'right',
};
export default class DatabaseQueryModifiers {
constructor() {
this._limit = undefined;
this._orderBy = undefined;
this._startAt = undefined;
this._endAt = undefined;
this._modifiers = [];
}
/**
*
* LIMIT
*
*/
hasLimit() {
return this._limit !== undefined;
}
isValidLimit(limit) {
return !isNumber(limit) || Math.floor(limit) !== limit || limit <= 0;
}
limitToFirst(limit) {
const newLimit = {
id: `limit-limitToFirst:${limit}`,
name: 'limitToFirst',
type: 'limit',
value: limit,
viewFrom: CONSTANTS.VIEW_FROM_LEFT,
};
this._limit = newLimit;
this._modifiers.push(newLimit);
return this;
}
limitToLast(limit) {
const newLimit = {
id: `limit-limitToLast:${limit}`,
name: 'limitToLast',
type: 'limit',
value: limit,
viewFrom: CONSTANTS.VIEW_FROM_RIGHT,
};
this._limit = newLimit;
this._modifiers.push(newLimit);
return this;
}
/**
*
* ORDER
*
*/
hasOrderBy() {
return this._orderBy !== undefined;
}
orderByChild(path) {
const newOrder = {
id: `order-orderByChild:${path}`,
type: 'orderBy',
name: 'orderByChild',
key: path,
};
this._orderBy = newOrder;
this._modifiers.push(newOrder);
return this;
}
orderByKey() {
const newOrder = {
id: `order-orderByKey`,
type: 'orderBy',
name: 'orderByKey',
};
this._orderBy = newOrder;
this._modifiers.push(newOrder);
return this;
}
isValidPriority(priority) {
return isNumber(priority) || isString(priority) || isNull(priority);
}
orderByPriority() {
const newOrder = {
id: `order-orderByPriority`,
type: 'orderBy',
name: 'orderByPriority',
};
this._orderBy = newOrder;
this._modifiers.push(newOrder);
return this;
}
orderByValue() {
const newOrder = {
id: `order-orderByValue`,
type: 'orderBy',
name: 'orderByValue',
};
this._orderBy = newOrder;
this._modifiers.push(newOrder);
return this;
}
/**
*
* FILTER
*
*/
hasStartAt() {
return this._startAt !== undefined;
}
hasEndAt() {
return this._endAt !== undefined;
}
startAt(value, key) {
const newStart = {
id: `filter-startAt:${value}:${key || ''}`,
type: 'filter',
name: 'startAt',
value,
valueType: value === null ? 'null' : typeof value,
key,
};
this._startAt = newStart;
this._modifiers.push(newStart);
return this;
}
endAt(value, key) {
const newStart = {
id: `filter-endAt:${value}:${key || ''}`,
type: 'filter',
name: 'endAt',
value,
valueType: value === null ? 'null' : typeof value,
key,
};
this._endAt = newStart;
this._modifiers.push(newStart);
return this;
}
// Returns a modifier array
toArray() {
return this._modifiers;
}
// Converts the modifier list to a string representation
toString() {
const sorted = this._modifiers.sort((a, b) => {
if (a.id < b.id) return -1;
if (a.id > b.id) return 1;
return 0;
});
let key = '{';
for (let i = 0; i < sorted.length; i++) {
if (i !== 0) key += ',';
key += sorted[i].id;
}
key += '}';
return key;
}
validateModifiers(prefix) {
if (this._orderBy && this._orderBy.name === 'orderByKey') {
if ((this._startAt && !!this._startAt.key) || (this._endAt && !!this._endAt.key)) {
throw new Error(
`${prefix} When ordering by key, you may only pass a value argument to startAt(), endAt(), or equalTo().`,
);
}
}
if (this._orderBy && this._orderBy.name === 'orderByKey') {
if (
(this._startAt && this._startAt.valueType !== 'string') ||
(this._endAt && this._endAt.valueType !== 'string')
) {
throw new Error(
`${prefix} When ordering by key, the value of startAt(), endAt(), or equalTo() must be a string.`,
);
}
}
if (this._orderBy && this._orderBy.name === 'orderByPriority') {
if (
(this._startAt && !this.isValidPriority(this._startAt.value)) ||
(this._endAt && !this.isValidPriority(this._endAt.value))
) {
throw new Error(
`${prefix} When ordering by priority, the first value of startAt(), endAt(), or equalTo() must be a valid priority value (null, a number, or a string).`,
);
}
}
}
}

View File

@@ -0,0 +1,269 @@
/* eslint-disable no-console */
/*
* 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 {
isString,
pathParent,
pathChild,
isValidPath,
// generateDatabaseId,
isNumber,
isNull,
isUndefined,
isFunction,
promiseWithOptionalCallback,
isObject,
isBoolean,
} from '@react-native-firebase/common';
import DatabaseQuery from './DatabaseQuery';
import DatabaseQueryModifiers from './DatabaseQueryModifiers';
import DatabaseOnDisconnect from './DatabaseOnDisconnect';
import DatabaseDataSnapshot from './DatabaseDataSnapshot';
const internalRefs = ['.info/connected', '.info/serverTimeOffset'];
export default class DatabaseReference extends DatabaseQuery {
constructor(database, path) {
// Validate the reference path
if (!internalRefs.includes(path) && !isValidPath(path)) {
throw new Error(
`firebase.database() Paths must be non-empty strings and can't contain ".", "#", "$", "[", or "]"`,
);
}
super(database, path, new DatabaseQueryModifiers());
this._database = database;
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.Reference.html#parent
*/
get parent() {
const parentPath = pathParent(this.path);
if (parentPath === null) return null;
return new DatabaseReference(this._database, parentPath);
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.Reference.html#root
*/
get root() {
return new DatabaseReference(this._database, '/');
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.Reference.html#child
* @param path
*/
child(path) {
if (!isString(path)) {
throw new Error(`firebase.database().ref().child(*) 'path' must be a string value.`);
}
return new DatabaseReference(this._database, pathChild(this.path, path));
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.Reference.html#set
* @param value
* @param onComplete
*/
set(value, onComplete) {
if (isUndefined(value)) {
throw new Error(`firebase.database().ref().set(*) 'value' must be defined.`);
}
if (!isUndefined(onComplete) && !isFunction(onComplete)) {
throw new Error(
`firebase.database().ref().set(_, *) 'onComplete' must be a function if provided.`,
);
}
return promiseWithOptionalCallback(this._database.native.set(this.path, { value }), onComplete);
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.Reference.html#update
* @param values
* @param onComplete
*/
update(values, onComplete) {
if (!isObject(values)) {
throw new Error(`firebase.database().ref().update(*) 'values' must be an object.`);
}
if (!Object.keys(values).length) {
throw new Error(
`firebase.database().ref().update(*) 'values' must be an object containing multiple values.`,
);
}
// TODO validate keys
if (!isUndefined(onComplete) && !isFunction(onComplete)) {
throw new Error(
`firebase.database().ref().update(_, *) 'onComplete' must be a function if provided.`,
);
}
return promiseWithOptionalCallback(
this._database.native.update(this.path, { values }),
onComplete,
);
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.Reference#setwithpriority
* @param newVal
* @param newPriority
* @param onComplete
*/
setWithPriority(newVal, newPriority, onComplete) {
if (isUndefined(newVal)) {
throw new Error(`firebase.database().ref().setWithPriority(*) 'newVal' must be defined.`);
}
if (!isNumber(newPriority) && !isString(newPriority) && !isNull(newPriority)) {
throw new Error(
`firebase.database().ref().setWithPriority(_, *) 'newPriority' must be a number, string or null value.`,
);
}
if (!isUndefined(onComplete) && !isFunction(onComplete)) {
throw new Error(
`firebase.database().ref().setWithPriority(_, _, *) 'onComplete' must be a function if provided.`,
);
}
return promiseWithOptionalCallback(
this._database.native.setWithPriority(this.path, {
value: newVal,
priority: newPriority,
}),
onComplete,
);
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.Reference#remove
* @param onComplete
*/
remove(onComplete) {
if (!isUndefined(onComplete) && !isFunction(onComplete)) {
throw new Error(
`firebase.database().ref().remove(*) 'onComplete' must be a function if provided.`,
);
}
return promiseWithOptionalCallback(this._database.native.remove(this.path), onComplete);
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.Reference#transaction
* @param transactionUpdate
* @param onComplete
* @param applyLocally
*/
transaction(transactionUpdate, onComplete, applyLocally) {
if (!isFunction(transactionUpdate)) {
throw new Error(
`firebase.database().ref().transaction(*) 'transactionUpdate' must be a function.`,
);
}
if (!isUndefined(onComplete) && !isFunction(onComplete)) {
throw new Error(
`firebase.database().ref().transaction(_, *) 'onComplete' must be a function if provided.`,
);
}
if (!isUndefined(applyLocally) && !isBoolean(applyLocally)) {
throw new Error(
`firebase.database().ref().transaction(_, _, *) 'applyLocally' must be a boolean value if provided.`,
);
}
return new Promise((resolve, reject) => {
const onCompleteWrapper = (error, committed, snapshotData) => {
if (isFunction(onComplete)) {
if (error) {
onComplete(error, committed, null);
} else {
onComplete(null, committed, new DatabaseDataSnapshot(this, snapshotData));
}
}
if (error) return reject(error);
return resolve({
committed,
snapshot: new DatabaseDataSnapshot(this, snapshotData),
});
};
// start the transaction natively
this._database._transaction.add(this, transactionUpdate, onCompleteWrapper, applyLocally);
});
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.Reference#setpriority
* @param priority
* @param onComplete
*/
setPriority(priority, onComplete) {
if (!isNumber(priority) && !isString(priority) && !isNull(priority)) {
throw new Error(
`firebase.database().ref().setPriority(*) 'priority' must be a number, string or null value.`,
);
}
if (!isUndefined(onComplete) && !isFunction(onComplete)) {
throw new Error(
`firebase.database().ref().setPriority(_, *) 'onComplete' must be a function if provided.`,
);
}
return promiseWithOptionalCallback(
this._database.native.setPriority(this.path, { priority }),
onComplete,
);
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.Reference#push
* @param value
* @param onComplete
* @returns {DatabaseReference}
*/
// push(value, onComplete) {
// TODO validate value?
//
// const id = generateDatabaseId(this._database._serverTime);
// const pushRef = this.child(id);
// const thennablePushRef = this.child(id);
//
// return thennablePushRef;
// }
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.Reference#ondisconnect
*/
onDisconnect() {
return new DatabaseOnDisconnect(this);
}
}

View File

@@ -0,0 +1,24 @@
/*
* 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.
*
*/
export default {
ServerValue: {
TIMESTAMP: {
'.sv': 'timestamp',
},
},
};

View File

@@ -0,0 +1,309 @@
/*
* 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 { NativeModules } from 'react-native';
import NativeError from '@react-native-firebase/app/lib/internal/NativeFirebaseError';
import { isString } from '@react-native-firebase/common';
import SharedEventEmitter from '@react-native-firebase/app/lib/internal/SharedEventEmitter';
import DatabaseDataSnapshot from './DatabaseDataSnapshot';
class DatabaseSyncTree {
constructor() {
this._tree = {};
this._reverseLookup = {};
SharedEventEmitter.addListener('database_sync_event', this._handleSyncEvent.bind(this));
}
get native() {
return NativeModules.RNFBDatabaseQueryModule;
}
/**
* Handles an incoming event from native
* @param event
* @private
*/
_handleSyncEvent(event) {
const { body } = event;
if (body.error) {
this._handleErrorEvent(body);
} else {
this._handleValueEvent(body);
}
}
/**
* Routes native database query listener cancellation events to their js counterparts.
*
* @param event
* @private
*/
_handleErrorEvent(event) {
// console.log('SyncTree.ERROR >>>', event);
const { eventRegistrationKey, registrationCancellationKey } = event.registration;
const registration = this.getRegistration(registrationCancellationKey);
if (registration) {
// build a new js error - we additionally attach
// the ref as a property for easier debugging
const error = NativeError.fromEvent(event.error, 'database');
// forward on to users .on(successCallback, cancellationCallback <-- listener
SharedEventEmitter.emit(registrationCancellationKey, error);
// remove the paired event registration - if we received a cancellation
// event then it's guaranteed that they'll be no further value events
this.removeRegistration(eventRegistrationKey);
}
}
/**
* Routes native database 'on' events to their js equivalent counterpart.
* If t is no longer any listeners remaining for this event we internally
* call the native unsub method to prevent further events coming through.
*
* @param event
* @private
*/
_handleValueEvent(event) {
// console.log('SyncTree.VALUE >>>', event);
const { key, eventRegistrationKey } = event.registration;
const registration = this.getRegistration(eventRegistrationKey);
// console.log('SyncTree.registration >>>', registration);
if (!registration) {
// registration previously revoked
// notify native that the registration
// no longer exists so it can remove
// the native listeners
return this.native.off(key, eventRegistrationKey);
}
let snapshot;
let previousChildName;
// Value events don't return a previousChildName
if (event.eventType === 'value') {
snapshot = new DatabaseDataSnapshot(registration.ref, event.data);
} else {
snapshot = new DatabaseDataSnapshot(registration.ref, event.data.snapshot);
// eslint-disable-next-line prefer-destructuring
previousChildName = event.data.previousChildName;
}
// forward on to users .on(successCallback <-- listener
return SharedEventEmitter.emit(eventRegistrationKey, snapshot, previousChildName);
}
/**
* Returns registration information such as appName, ref, path and registration keys.
*
* @param registration
* @return {null}
*/
getRegistration(registration: string) {
return this._reverseLookup[registration]
? Object.assign({}, this._reverseLookup[registration])
: null;
}
/**
* Removes all listeners for the specified registration keys.
*
* @param registrations
* @return {number}
*/
removeListenersForRegistrations(registrations) {
if (isString(registrations)) {
this.removeRegistration(registrations);
SharedEventEmitter.removeAllListeners(registrations);
return 1;
}
if (!Array.isArray(registrations)) return 0;
for (let i = 0, len = registrations.length; i < len; i++) {
this.removeRegistration(registrations[i]);
SharedEventEmitter.removeAllListeners(registrations[i]);
}
return registrations.length;
}
/**
* Removes a specific listener from the specified registrations.
*
* @param listener
* @param registrations
* @return {Array} array of registrations removed
*/
removeListenerRegistrations(listener, registrations) {
if (!Array.isArray(registrations)) return [];
const removed = [];
for (let i = 0, len = registrations.length; i < len; i++) {
const registration = registrations[i];
const subscriptions = SharedEventEmitter._subscriber.getSubscriptionsForType(registration);
if (subscriptions) {
for (let j = 0, l = subscriptions.length; j < l; j++) {
const subscription = subscriptions[j];
// The subscription may have been removed during this event loop.
// its listener matches the listener in method parameters
if (subscription && subscription.listener === listener) {
subscription.remove();
removed.push(registration);
this.removeRegistration(registration);
}
}
}
}
return removed;
}
/**
* Returns an array of all registration keys for the specified path.
*
* @param path
* @return {Array}
*/
getRegistrationsByPath(path) {
const out = [];
const eventKeys = Object.keys(this._tree[path] || {});
for (let i = 0, len = eventKeys.length; i < len; i++) {
Array.prototype.push.apply(out, Object.keys(this._tree[path][eventKeys[i]]));
}
return out;
}
/**
* Returns an array of all registration keys for the specified path and eventType.
*
* @param path
* @param eventType
* @return {Array}
*/
getRegistrationsByPathEvent(path, eventType) {
if (!this._tree[path]) return [];
if (!this._tree[path][eventType]) return [];
return Object.keys(this._tree[path][eventType]);
}
/**
* Returns a single registration key for the specified path, eventType, and listener
*
* @param path
* @param eventType
* @param listener
* @return {Array}
*/
getOneByPathEventListener(path, eventType, listener) {
if (!this._tree[path]) return null;
if (!this._tree[path][eventType]) return null;
const registrationsForPathEvent = Object.entries(this._tree[path][eventType]);
for (let i = 0; i < registrationsForPathEvent.length; i++) {
const registration = registrationsForPathEvent[i];
if (registration[1] === listener) return registration[0];
}
return null;
}
/**
* Register a new listener.
*
* @param registration
*/
addRegistration(registration) {
const { eventRegistrationKey, eventType, listener, once, path } = registration;
if (!this._tree[path]) this._tree[path] = {};
if (!this._tree[path][eventType]) this._tree[path][eventType] = {};
this._tree[path][eventType][eventRegistrationKey] = listener;
this._reverseLookup[eventRegistrationKey] = registration;
if (once) {
SharedEventEmitter.once(
eventRegistrationKey,
this._onOnceRemoveRegistration(eventRegistrationKey, listener),
);
} else {
SharedEventEmitter.addListener(eventRegistrationKey, listener);
}
return eventRegistrationKey;
}
/**
* Remove a registration, if it's not a `once` registration then instructs native
* to also remove the underlying database query listener.
*
* @param registration
* @return {boolean}
*/
removeRegistration(registration) {
if (!this._reverseLookup[registration]) return false;
const { path, eventType, once } = this._reverseLookup[registration];
if (!this._tree[path]) {
delete this._reverseLookup[registration];
return false;
}
if (!this._tree[path][eventType]) {
delete this._reverseLookup[registration];
return false;
}
// we don't want `once` events to notify native as they're already
// automatically unsubscribed on native when the first event is sent
const registrationObj = this._reverseLookup[registration];
if (registrationObj && !once) {
this.native.off(registrationObj.key, registration);
}
delete this._tree[path][eventType][registration];
delete this._reverseLookup[registration];
return !!registrationObj;
}
/**
* Wraps a `once` listener with a new function that self de-registers.
*
* @param registration
* @param listener
* @return {function(...[*])}
* @private
*/
_onOnceRemoveRegistration(registration, listener) {
return (...args) => {
this.removeRegistration(registration);
listener(...args);
};
}
}
export default new DatabaseSyncTree();

View File

@@ -0,0 +1,176 @@
/* eslint-disable no-console */
/*
* 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 NativeError from '@react-native-firebase/app/lib/internal/NativeFirebaseError';
let transactionId = 0;
/**
* Uses the push id generator to create a transaction id
* @returns {number}
* @private
*/
const generateTransactionId = () => transactionId++;
export default class DatabaseTransaction {
constructor(database) {
this._database = database;
this._emitter = database.emitter;
this._transactions = {};
this._emitter.addListener(
this._database.eventNameForApp('database_transaction_event'),
this._onTransactionEvent.bind(this),
);
}
/**
*
* @param reference
* @param transactionUpdater
* @param onComplete
* @param applyLocally
*/
add(reference, transactionUpdater, onComplete, applyLocally = false) {
const id = generateTransactionId();
this._transactions[id] = {
id,
reference,
transactionUpdater,
onComplete,
applyLocally,
completed: false,
started: true,
};
this._database.native.transactionStart(reference.path, id, applyLocally);
}
/**
* Returns a transaction by ID
*
* @param id
* @return {*}
* @private
*/
_getTransaction(id) {
return this._transactions[id];
}
/**
* Removes a transaction by ID on the next event loop
*
* @param id
* @private
*/
_removeTransaction(id) {
setImmediate(() => {
delete this._transactions[id];
});
}
/**
*
* @param event
* @private
*/
_onTransactionEvent(event) {
switch (event.body.type) {
case 'update':
return this._handleUpdate(event);
case 'error':
return this._handleError(event);
case 'complete':
return this._handleComplete(event);
default:
return undefined;
}
}
/**
*
* @param event
* @private
*/
_handleUpdate(event) {
let newValue;
const { id, body } = event;
const { value } = body;
try {
const transaction = this._getTransaction(id);
if (!transaction) return;
newValue = transaction.transactionUpdater(value);
} finally {
let abort = false;
if (newValue === undefined) {
abort = true;
}
this._database.native.transactionTryCommit(id, {
value: newValue,
abort,
});
}
}
/**
*
* @param event
* @private
*/
_handleError(event) {
const transaction = this._getTransaction(event.id);
if (transaction && !transaction.completed) {
transaction.completed = true;
try {
// error, committed, snapshot
const error = NativeError.fromEvent(event.body.error, 'database');
transaction.onComplete(error, false, null);
} finally {
this._removeTransaction(event.id);
}
}
}
/**
*
* @param event
* @private
*/
_handleComplete(event) {
const transaction = this._getTransaction(event.id);
if (transaction && !transaction.completed) {
transaction.completed = true;
try {
// error, committed, snapshot
transaction.onComplete(null, event.body.committed, Object.assign({}, event.body.snapshot));
} finally {
this._removeTransaction(event.id);
}
}
}
}

1204
packages/database/lib/index.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,199 @@
/*
* 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 {
createModuleNamespace,
FirebaseModule,
getFirebaseRoot,
} from '@react-native-firebase/app/lib/internal';
import { isString, isBoolean, isNumber } from '@react-native-firebase/common';
import version from './version';
import DatabaseStatics from './DatabaseStatics';
import DatabaseReference from './DatabaseReference';
import DatabaseTransaction from './DatabaseTransaction';
const namespace = 'database';
const nativeModuleName = [
'RNFBDatabaseModule',
'RNFBDatabaseReferenceModule',
'RNFBDatabaseQueryModule',
'RNFBDatabaseOnDisconnectModule',
'RNFBDatabaseTransactionModule',
];
class FirebaseDatabaseModule extends FirebaseModule {
constructor(app, config, databaseUrl) {
super(app, config, databaseUrl);
this._serverTimeOffset = 0;
this._customUrlOrRegion = databaseUrl || this.app.options.databaseURL;
this._transaction = new DatabaseTransaction(this);
setTimeout(() => {
this._syncServerTimeOffset();
}, 100);
}
/**
* Keep the server time offset in sync with the server time
* @private
*/
_syncServerTimeOffset() {
this.ref('.info/serverTimeOffset').on('value', snapshot => {
this._serverTimeOffset = snapshot.val();
});
}
/**
* Get the current server time, used to generate data such as database keys
* @returns {Date}
* @private
*/
get _serverTime() {
return new Date(Date.now() + this._serverTimeOffset);
}
/**
* Returns a new Reference instance from a given path. Defaults to the root reference.
* @param path
* @returns {DatabaseReference}
*/
ref(path = '/') {
if (!isString(path)) {
throw new Error(`firebase.app().database().ref(*) 'path' must be a string value.`);
}
return new DatabaseReference(this, path);
}
/**
* Generates a Reference from a database URL.
* Note domain must be the same.
* Any query parameters are stripped as per the web SDK.
* @param url
* @returns {DatabaseReference}
*/
refFromURL(url) {
if (!isString(url) || !url.startsWith('https://')) {
throw new Error(
`firebase.app().database().refFromURL(*) 'url' must be a valid database URL.`,
);
}
if (!url.includes(this._customUrlOrRegion)) {
throw new Error(
`firebase.app().database().refFromURL(*) 'url' must be the same domain as the current instance (${
this._customUrlOrRegion
}). To use a different database domain, create a new Firebase instance.`,
);
}
let path = url.replace(this._customUrlOrRegion, '');
if (path.includes('?')) path = path.slice(0, path.indexOf('?'));
return new DatabaseReference(this, path || '/');
}
/**
* goOnline
*/
goOnline() {
return this.native.goOnline();
}
/**
* goOffline
*/
goOffline() {
return this.native.goOffline();
}
/**
*
* @param enabled
*/
setPersistenceEnabled(enabled) {
if (!isBoolean(enabled)) {
throw new Error(
`firebase.app().database().setPersistenceEnabled(*) 'enabled' must be a boolean value.`,
);
}
this.native.setPersistenceEnabled(enabled);
}
/**
*
* @param enabled
*/
setLoggingEnabled(enabled) {
if (!isBoolean(enabled)) {
throw new Error(
`firebase.app().database().setLoggingEnabled(*) 'enabled' must be a boolean value.`,
);
}
this.native.setLoggingEnabled(enabled);
}
/**
*
* @param bytes
*/
setPersistenceCacheSizeBytes(bytes) {
if (!isNumber(bytes)) {
throw new Error(
`firebase.app().database().setPersistenceCacheSizeBytes(*) 'bytes' must be a number value.`,
);
}
if (bytes < 1000000) {
throw new Error(
`firebase.app().database().setPersistenceCacheSizeBytes(*) 'bytes' must be greater than 1000000 (1MB).`,
);
}
if (bytes > 100000000) {
throw new Error(
`firebase.app().database().setPersistenceCacheSizeBytes(*) 'bytes' must be less than 100000000 (10MB).`,
);
}
this.native.setPersistenceCacheSizeBytes(bytes);
}
}
// import { SDK_VERSION } from '@react-native-firebase/database';
export const SDK_VERSION = version;
// import database from '@react-native-firebase/database';
// database().X(...);
export default createModuleNamespace({
statics: DatabaseStatics,
version,
namespace,
nativeModuleName,
nativeEvents: ['database_transaction_event', 'database_sync_event'],
hasMultiAppSupport: true,
hasCustomUrlOrRegionSupport: true,
ModuleClass: FirebaseDatabaseModule,
});
// import database, { firebase } from '@react-native-firebase/database';
// database().X(...);
// firebase.database().X(...);
export const firebase = getFirebaseRoot();

View File

@@ -0,0 +1,68 @@
/* eslint-disable import/no-duplicates */
/*
* 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 type { ReactNativeFirebaseModule } from '@react-native-firebase/app-types/index.js.flow';
export interface Statics {}
export interface Module extends ReactNativeFirebaseModule {}
declare module '@react-native-firebase/database' {
import type {
ReactNativeFirebaseNamespace,
ReactNativeFirebaseModuleAndStatics,
} from '@react-native-firebase/app-types/index.js.flow';
/**
* @example
* ```js
* import { firebase } from '@react-native-firebase/database';
* firebase.database().X(...);
* ```
*/
declare export var firebase: {} & ReactNativeFirebaseNamespace;
/**
* @example
* ```js
* import database from '@react-native-firebase/database';
* database().X(...);
* ```
*/
declare export default ReactNativeFirebaseModuleAndStatics<Module, Statics>;
}
/**
* Attach namespace to `firebase.` and `FirebaseApp.`.
*/
declare module '@react-native-firebase/app-types' {
import type { ReactNativeFirebaseModuleAndStatics } from '@react-native-firebase/app-types/index.js.flow';
declare interface ReactNativeFirebaseNamespace {
/**
* Database
*/
database: ReactNativeFirebaseModuleAndStatics<Module, Statics>;
}
declare interface FirebaseApp {
/**
* Database
*/
database(): Module;
}
}