[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

@@ -17,7 +17,7 @@
*/
import binaryToBase64 from 'react-native/Libraries/Utilities/binaryToBase64';
import promiseDefer from './promiseDefer';
import { promiseDefer } from './promise';
const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';

View File

@@ -0,0 +1,27 @@
import { isObject } from './validate';
/**
* Deep get a value from an object.
* @website https://github.com/Salakar/deeps
* @param object
* @param path
* @param joiner
* @returns {*}
*/
// eslint-disable-next-line import/prefer-default-export
export function deepGet(object, path, joiner = '/') {
if (!isObject(object) && !Array.isArray(object)) return undefined;
const keys = path.split(joiner);
let i = 0;
let tmp = object;
const len = keys.length;
while (i < len) {
const key = keys[i++];
if (!tmp || !Object.hasOwnProperty.call(tmp, key)) return undefined;
tmp = tmp[key];
}
return tmp;
}

72
packages/common/lib/id.js Normal file
View File

@@ -0,0 +1,72 @@
const PUSH_CHARS = '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz';
const AUTO_ID_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
// timestamp of last push, used to prevent local collisions if you push twice in one ms.
let lastPushTime = 0;
// we generate 72-bits of randomness which get turned into 12 characters and appended to the
// timestamp to prevent collisions with other clients. We store the last characters we
// generated because in the event of a collision, we'll use those same characters except
// "incremented" by one.
const lastRandChars = [];
/**
* Generate a firebase id - for use with ref().push(val, cb) - e.g. -KXMr7k2tXUFQqiaZRY4'
* @param serverTimeOffset - pass in server time offset from native side
* @returns {string}
*/
export function generateDatabaseId(serverTimeOffset = 0): string {
const timeStampChars = new Array(8);
let now = new Date().getTime() + serverTimeOffset;
const duplicateTime = now === lastPushTime;
lastPushTime = now;
for (let i = 7; i >= 0; i -= 1) {
timeStampChars[i] = PUSH_CHARS.charAt(now % 64);
now = Math.floor(now / 64);
}
if (now !== 0) {
throw new Error('We should have converted the entire timestamp.');
}
let id = timeStampChars.join('');
if (!duplicateTime) {
for (let i = 0; i < 12; i += 1) {
lastRandChars[i] = Math.floor(Math.random() * 64);
}
} else {
// if the timestamp hasn't changed since last push,
// use the same random number, but increment it by 1.
let i;
for (i = 11; i >= 0 && lastRandChars[i] === 63; i -= 1) {
lastRandChars[i] = 0;
}
lastRandChars[i] += 1;
}
for (let i = 0; i < 12; i++) {
id += PUSH_CHARS.charAt(lastRandChars[i]);
}
if (id.length !== 20) throw new Error('Length should be 20.');
return id;
}
/**
* Generate a firestore auto id for use with collection/document .add()
* @return {string}
*/
export function generateFirestoreId(): string {
let autoId = '';
for (let i = 0; i < 20; i++) {
autoId += AUTO_ID_CHARS.charAt(Math.floor(Math.random() * AUTO_ID_CHARS.length));
}
return autoId;
}

View File

@@ -18,10 +18,11 @@ import { Platform } from 'react-native';
import { isString } from './validate';
import Base64 from './Base64';
export * from './id';
export * from './path';
export * from './validate';
export * from './promise';
export Base64 from './Base64';
export promiseDefer from './promiseDefer';
export ReferenceBase from './ReferenceBase';
export function getDataUrlParts(dataUrlString) {
@@ -74,3 +75,19 @@ export function stripTrailingSlash(string) {
export const isIOS = Platform.OS === 'ios';
export const isAndroid = Platform.OS === 'android';
export function tryJSONParse(string) {
try {
return string && JSON.parse(string);
} catch (jsonError) {
return string;
}
}
export function tryJSONStringify(data: mixed): string | null {
try {
return JSON.stringify(data);
} catch (jsonError) {
return null;
}
}

View File

@@ -35,10 +35,7 @@ export function pathParent(path) {
* Joins a parent and a child path
*/
export function pathChild(path, childPath) {
const canonicalChildPath = childPath
.split('/')
.filter($ => $.length > 0)
.join('/');
const canonicalChildPath = pathPieces(childPath).join('/');
if (path.length === 0) {
return canonicalChildPath;
@@ -58,3 +55,59 @@ export function pathLastComponent(path) {
return path.slice(index + 1);
}
/**
* Returns all none empty pieces of the path
* @param path
* @returns {*}
*/
export function pathPieces(path) {
return path.split('/').filter($ => $.length > 0);
}
/**
* Returns whether a given path is empty
* @param path
* @returns {boolean}
*/
export function pathIsEmpty(path) {
return !pathPieces(path).length;
}
/**
* Converts a given path to a URL encoded string
* @param path
* @returns {string|string}
*/
export function pathToUrlEncodedString(path) {
const pieces = pathPieces(path);
let pathString = '';
for (let i = 0; i < pieces.length; i++) {
pathString += `/${encodeURIComponent(String(pieces[i]))}`;
}
return pathString || '/';
}
// eslint-disable-next-line no-control-regex
export const INVALID_PATH_REGEX = /[[\].#$\u0000-\u001F\u007F]/;
/**
* Ensures a given path is a valid Firebase path
* @param path
* @returns {boolean}
*/
export function isValidPath(path) {
return typeof path === 'string' && path.length !== 0 && !INVALID_PATH_REGEX.test(path);
}
// eslint-disable-next-line no-control-regex,no-useless-escape
export const INVALID_KEY_REGEX = /[\[\].#$\/\u0000-\u001F\u007F]/;
/**
* Ensures a given key is a valid Firebase key
* @param key
* @returns {boolean}
*/
export function isValidKey(key) {
return typeof key === 'string' && key.length !== 0 && !INVALID_KEY_REGEX.test(path);
}

View File

@@ -15,7 +15,12 @@
*
*/
export default function promiseDefer() {
import { isFunction } from './validate';
/**
*
*/
export function promiseDefer() {
const deferred = {
resolve: null,
reject: null,
@@ -28,3 +33,26 @@ export default function promiseDefer() {
return deferred;
}
/**
* @param promise
* @param callback
*/
export function promiseWithOptionalCallback(promise, callback) {
if (!isFunction(callback)) return promise;
return promise
.then(result => {
if (callback && callback.length === 1) {
callback(null);
} else if (callback) {
callback(null, result);
}
return result;
})
.catch(error => {
if (callback) callback(error);
return Promise.reject(error);
});
}

View File

@@ -0,0 +1,41 @@
/*
* 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 { tryJSONParse, tryJSONStringify } from './index';
import { isObject } from './validate';
export function serializeType(value) {
if (isObject(value)) {
return {
type: 'object',
value: serializeObject(value),
};
}
return {
type: typeof value,
value,
};
}
export function serializeObject(object) {
if (!isObject(object)) return object;
// json stringify then parse it calls toString on Objects / Classes
// that support it i.e new Date() becomes a ISO string.
return tryJSONParse(tryJSONStringify(object));
}