ran-boilerplate
Version:
React . Apollo (GraphQL) . Next.js Toolkit
287 lines (286 loc) • 11 kB
JavaScript
"use strict";
// The MIT License (MIT)
//
// Copyright (c) 2017 Firebase
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
Object.defineProperty(exports, "__esModule", { value: true });
const _ = require("lodash");
const apps_1 = require("../apps");
const cloud_functions_1 = require("../cloud-functions");
const utils_1 = require("../utils");
const index_1 = require("../index");
/** @internal */
exports.provider = 'google.firebase.database';
// NOTE(inlined): Should we relax this a bit to allow staging or alternate implementations of our API?
const databaseURLRegex = new RegExp('https://([^.]+).firebaseio.com');
/**
* Pick the Realtime Database instance to use. If omitted, will pick the default database for your project.
*/
function instance(instance) {
return new InstanceBuilder(instance);
}
exports.instance = instance;
class InstanceBuilder {
/* @internal */
constructor(instance) {
this.instance = instance;
}
ref(path) {
return new RefBuilder(apps_1.apps(), `projects/_/instances/${this.instance}/refs/${path}`);
}
}
exports.InstanceBuilder = InstanceBuilder;
/**
* Handle events at a Firebase Realtime Database Reference.
*
* This method behaves very similarly to the method of the same name in the
* client and Admin Firebase SDKs. Any change to the Database that affects the
* data at or below the provided `path` will fire an event in Cloud Functions.
*
* There are three important differences between listening to a Realtime
* Database event in Cloud Functions and using the Realtime Database in the
* client and Admin SDKs:
* 1. Cloud Functions allows wildcards in the `path` name. Any `path` component
* in curly brackets (`{}`) is a wildcard that matches all strings. The value
* that matched a certain invocation of a Cloud Function is returned as part
* of the `event.params` object. For example, `ref("messages/{messageId}")`
* matches changes at `/messages/message1` or `/messages/message2`, resulting
* in `event.params.messageId` being set to `"message1"` or `"message2"`,
* respectively.
* 2. Cloud Functions do not fire an event for data that already existed before
* the Cloud Function was deployed.
* 3. Cloud Function events have access to more information, including a
* snapshot of the previous event data and information about the user who
* triggered the Cloud Function.
*/
function ref(path) {
const normalized = utils_1.normalizePath(path);
const databaseURL = index_1.config().firebase.databaseURL;
if (!databaseURL) {
throw new Error('Missing expected config value firebase.databaseURL, ' +
'config is actually' + JSON.stringify(index_1.config()));
}
const match = databaseURL.match(databaseURLRegex);
if (!match) {
throw new Error('Invalid value for config firebase.databaseURL: ' + databaseURL);
}
const subdomain = match[1];
let resource = `projects/_/instances/${subdomain}/refs/${normalized}`;
return new RefBuilder(apps_1.apps(), resource);
}
exports.ref = ref;
/** Builder used to create Cloud Functions for Firebase Realtime Database References. */
class RefBuilder {
/** @internal */
constructor(apps, resource) {
this.apps = apps;
this.resource = resource;
}
/** Respond to any write that affects a ref. */
onWrite(handler) {
return this.onOperation(handler, 'ref.write');
}
/** Respond to new data on a ref. */
onCreate(handler) {
return this.onOperation(handler, 'ref.create');
}
/** Respond to update on a ref. */
onUpdate(handler) {
return this.onOperation(handler, 'ref.update');
}
/** Respond to all data being deleted from a ref. */
onDelete(handler) {
return this.onOperation(handler, 'ref.delete');
}
onOperation(handler, eventType) {
const dataConstructor = (raw) => {
if (raw.data instanceof DeltaSnapshot) {
return raw.data;
}
let [dbInstance, path] = resourceToInstanceAndPath(raw.resource);
return new DeltaSnapshot(this.apps.forMode(raw.auth), this.apps.admin, raw.data.data, raw.data.delta, path, dbInstance);
};
return cloud_functions_1.makeCloudFunction({
provider: exports.provider, handler,
eventType: eventType,
resource: this.resource,
dataConstructor,
before: (event) => this.apps.retain(event),
after: (event) => this.apps.release(event),
});
}
}
exports.RefBuilder = RefBuilder;
/* Utility function to extract database reference from resource string */
/** @internal */
function resourceToInstanceAndPath(resource) {
let resourceRegex = `projects/([^/]+)/instances/([^/]+)/refs(/.+)?`;
let match = resource.match(new RegExp(resourceRegex));
if (!match) {
throw new Error(`Unexpected resource string for Firebase Realtime Database event: ${resource}. ` +
'Expected string in the format of "projects/_/instances/{firebaseioSubdomain}/refs/{ref=**}"');
}
let [, project, dbInstanceName, path] = match;
if (project !== '_') {
throw new Error(`Expect project to be '_' in a Firebase Realtime Database event`);
}
let dbInstance = 'https://' + dbInstanceName + '.firebaseio.com';
return [dbInstance, path];
}
exports.resourceToInstanceAndPath = resourceToInstanceAndPath;
class DeltaSnapshot {
constructor(app, adminApp, data, delta, path, // path will be undefined for the database root
instance) {
this.app = app;
this.adminApp = adminApp;
this.instance = instance;
if (delta !== undefined) {
this._path = path;
this._data = data;
this._delta = delta;
this._newData = utils_1.applyChange(this._data, this._delta);
}
}
get ref() {
if (!this._ref) {
this._ref = this.app.database(this.instance).ref(this._fullPath());
}
return this._ref;
}
get adminRef() {
if (!this._adminRef) {
this._adminRef = this.adminApp.database(this.instance).ref(this._fullPath());
}
return this._adminRef;
}
get key() {
let last = _.last(utils_1.pathParts(this._fullPath()));
return (!last || last === '') ? null : last;
}
val() {
let parts = utils_1.pathParts(this._childPath);
let source = this._isPrevious ? this._data : this._newData;
let node = _.cloneDeep(parts.length ? _.get(source, parts, null) : source);
return this._checkAndConvertToArray(node);
}
// TODO(inlined): figure out what to do here
exportVal() { return this.val(); }
// TODO(inlined): figure out what to do here
getPriority() {
return 0;
}
exists() {
return !_.isNull(this.val());
}
child(childPath) {
if (!childPath) {
return this;
}
return this._dup(this._isPrevious, childPath);
}
get previous() {
return this._isPrevious ? this : this._dup(true);
}
get current() {
return this._isPrevious ? this._dup(false) : this;
}
changed() {
return utils_1.valAt(this._delta, this._childPath) !== undefined;
}
forEach(action) {
let val = this.val();
if (_.isPlainObject(val)) {
return _.some(val, (value, key) => action(this.child(key)) === true);
}
return false;
}
hasChild(childPath) {
return this.child(childPath).exists();
}
hasChildren() {
let val = this.val();
return _.isPlainObject(val) && _.keys(val).length > 0;
}
numChildren() {
let val = this.val();
return _.isPlainObject(val) ? Object.keys(val).length : 0;
}
/**
* Prints the value of the snapshot; use '.previous.toJSON()' and '.current.toJSON()' to explicitly see
* the previous and current values of the snapshot.
*/
toJSON() {
return this.val();
}
/* Recursive function to check if keys are numeric & convert node object to array if they are */
_checkAndConvertToArray(node) {
if (node === null || typeof node === 'undefined') {
return null;
}
if (typeof node !== 'object') {
return node;
}
let obj = {};
let numKeys = 0;
let maxKey = 0;
let allIntegerKeys = true;
for (let key in node) {
if (!node.hasOwnProperty(key)) {
continue;
}
let childNode = node[key];
obj[key] = this._checkAndConvertToArray(childNode);
numKeys++;
const integerRegExp = /^(0|[1-9]\d*)$/;
if (allIntegerKeys && integerRegExp.test(key)) {
maxKey = Math.max(maxKey, Number(key));
}
else {
allIntegerKeys = false;
}
}
if (allIntegerKeys && maxKey < 2 * numKeys) {
// convert to array.
let array = [];
_.forOwn(obj, (val, key) => {
array[key] = val;
});
return array;
}
return obj;
}
_dup(previous, childPath) {
let dup = new DeltaSnapshot(this.app, this.adminApp, undefined, undefined, undefined, this.instance);
[dup._path, dup._data, dup._delta, dup._childPath, dup._newData] =
[this._path, this._data, this._delta, this._childPath, this._newData];
if (previous) {
dup._isPrevious = true;
}
if (childPath) {
dup._childPath = utils_1.joinPath(dup._childPath, childPath);
}
return dup;
}
_fullPath() {
let out = (this._path || '') + '/' + (this._childPath || '');
return out;
}
}
exports.DeltaSnapshot = DeltaSnapshot;