mirror of
https://github.com/titanscouting/tra-analysis.git
synced 2024-11-13 22:56:18 +00:00
316 lines
12 KiB
JavaScript
316 lines
12 KiB
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 config_1 = require("../config");
|
||
|
/** @internal */
|
||
|
exports.provider = 'google.firebase.database';
|
||
|
/** @internal */
|
||
|
exports.service = 'firebaseio.com';
|
||
|
// NOTE(inlined): Should we relax this a bit to allow staging or alternate implementations of our API?
|
||
|
const databaseURLRegex = new RegExp('https://([^.]+).firebaseio.com');
|
||
|
/**
|
||
|
* Selects a database instance that will trigger the function.
|
||
|
* If omitted, will pick the default database for your project.
|
||
|
* @param instance The Realtime Database instance to use.
|
||
|
*/
|
||
|
function instance(instance) {
|
||
|
return _instanceWithOpts(instance, {});
|
||
|
}
|
||
|
exports.instance = instance;
|
||
|
/**
|
||
|
* Select Firebase Realtime Database Reference to listen to.
|
||
|
*
|
||
|
* 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 `context.params` object. For example, `ref("messages/{messageId}")`
|
||
|
* matches changes at `/messages/message1` or `/messages/message2`, resulting
|
||
|
* in `context.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 information
|
||
|
* about the user who triggered the Cloud Function.
|
||
|
* @param ref Path of the database to listen to.
|
||
|
*/
|
||
|
function ref(path) {
|
||
|
return _refWithOpts(path, {});
|
||
|
}
|
||
|
exports.ref = ref;
|
||
|
/** @internal */
|
||
|
function _instanceWithOpts(instance, opts) {
|
||
|
return new InstanceBuilder(instance, opts);
|
||
|
}
|
||
|
exports._instanceWithOpts = _instanceWithOpts;
|
||
|
class InstanceBuilder {
|
||
|
/* @internal */
|
||
|
constructor(instance, opts) {
|
||
|
this.instance = instance;
|
||
|
this.opts = opts;
|
||
|
}
|
||
|
ref(path) {
|
||
|
const normalized = utils_1.normalizePath(path);
|
||
|
return new RefBuilder(apps_1.apps(), () => `projects/_/instances/${this.instance}/refs/${normalized}`, this.opts);
|
||
|
}
|
||
|
}
|
||
|
exports.InstanceBuilder = InstanceBuilder;
|
||
|
/** @internal */
|
||
|
function _refWithOpts(path, opts) {
|
||
|
const resourceGetter = () => {
|
||
|
const normalized = utils_1.normalizePath(path);
|
||
|
const databaseURL = config_1.firebaseConfig().databaseURL;
|
||
|
if (!databaseURL) {
|
||
|
throw new Error('Missing expected firebase config value databaseURL, ' +
|
||
|
'config is actually' +
|
||
|
JSON.stringify(config_1.firebaseConfig()) +
|
||
|
'\n If you are unit testing, please set process.env.FIREBASE_CONFIG');
|
||
|
}
|
||
|
const match = databaseURL.match(databaseURLRegex);
|
||
|
if (!match) {
|
||
|
throw new Error('Invalid value for config firebase.databaseURL: ' + databaseURL);
|
||
|
}
|
||
|
const subdomain = match[1];
|
||
|
return `projects/_/instances/${subdomain}/refs/${normalized}`;
|
||
|
};
|
||
|
return new RefBuilder(apps_1.apps(), resourceGetter, opts);
|
||
|
}
|
||
|
exports._refWithOpts = _refWithOpts;
|
||
|
/** Builder used to create Cloud Functions for Firebase Realtime Database References. */
|
||
|
class RefBuilder {
|
||
|
/** @internal */
|
||
|
constructor(apps, triggerResource, opts) {
|
||
|
this.apps = apps;
|
||
|
this.triggerResource = triggerResource;
|
||
|
this.opts = opts;
|
||
|
this.changeConstructor = (raw) => {
|
||
|
let [dbInstance, path] = resourceToInstanceAndPath(raw.context.resource.name);
|
||
|
let before = new DataSnapshot(raw.data.data, path, this.apps.admin, dbInstance);
|
||
|
let after = new DataSnapshot(utils_1.applyChange(raw.data.data, raw.data.delta), path, this.apps.admin, dbInstance);
|
||
|
return {
|
||
|
before: before,
|
||
|
after: after,
|
||
|
};
|
||
|
};
|
||
|
}
|
||
|
/** Respond to any write that affects a ref. */
|
||
|
onWrite(handler) {
|
||
|
return this.onOperation(handler, 'ref.write', this.changeConstructor);
|
||
|
}
|
||
|
/** Respond to update on a ref. */
|
||
|
onUpdate(handler) {
|
||
|
return this.onOperation(handler, 'ref.update', this.changeConstructor);
|
||
|
}
|
||
|
/** Respond to new data on a ref. */
|
||
|
onCreate(handler) {
|
||
|
let dataConstructor = (raw) => {
|
||
|
let [dbInstance, path] = resourceToInstanceAndPath(raw.context.resource.name);
|
||
|
return new DataSnapshot(raw.data.delta, path, this.apps.admin, dbInstance);
|
||
|
};
|
||
|
return this.onOperation(handler, 'ref.create', dataConstructor);
|
||
|
}
|
||
|
/** Respond to all data being deleted from a ref. */
|
||
|
onDelete(handler) {
|
||
|
let dataConstructor = (raw) => {
|
||
|
let [dbInstance, path] = resourceToInstanceAndPath(raw.context.resource.name);
|
||
|
return new DataSnapshot(raw.data.data, path, this.apps.admin, dbInstance);
|
||
|
};
|
||
|
return this.onOperation(handler, 'ref.delete', dataConstructor);
|
||
|
}
|
||
|
onOperation(handler, eventType, dataConstructor) {
|
||
|
return cloud_functions_1.makeCloudFunction({
|
||
|
handler,
|
||
|
provider: exports.provider,
|
||
|
service: exports.service,
|
||
|
eventType,
|
||
|
legacyEventType: `providers/${exports.provider}/eventTypes/${eventType}`,
|
||
|
triggerResource: this.triggerResource,
|
||
|
dataConstructor: dataConstructor,
|
||
|
before: event => this.apps.retain(),
|
||
|
after: event => this.apps.release(),
|
||
|
opts: this.opts,
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
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 DataSnapshot {
|
||
|
constructor(data, path, // path will be undefined for the database root
|
||
|
app, instance) {
|
||
|
this.app = app;
|
||
|
if (instance) {
|
||
|
// SDK always supplies instance, but user's unit tests may not
|
||
|
this.instance = instance;
|
||
|
}
|
||
|
else if (app) {
|
||
|
this.instance = app.options.databaseURL;
|
||
|
}
|
||
|
else if (process.env.GCLOUD_PROJECT) {
|
||
|
this.instance =
|
||
|
'https://' + process.env.GCLOUD_PROJECT + '.firebaseio.com';
|
||
|
}
|
||
|
this._path = path;
|
||
|
this._data = data;
|
||
|
}
|
||
|
/** Ref returns a reference to the database with full admin access. */
|
||
|
get ref() {
|
||
|
if (!this.app) {
|
||
|
// may be unpopulated in user's unit tests
|
||
|
throw new Error('Please supply a Firebase app in the constructor for DataSnapshot' +
|
||
|
' in order to use the .ref method.');
|
||
|
}
|
||
|
if (!this._ref) {
|
||
|
this._ref = this.app.database(this.instance).ref(this._fullPath());
|
||
|
}
|
||
|
return this._ref;
|
||
|
}
|
||
|
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._data;
|
||
|
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(childPath);
|
||
|
}
|
||
|
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(childPath) {
|
||
|
let dup = new DataSnapshot(this._data, undefined, this.app, this.instance);
|
||
|
[dup._path, dup._childPath] = [this._path, this._childPath];
|
||
|
if (childPath) {
|
||
|
dup._childPath = utils_1.joinPath(dup._childPath, childPath);
|
||
|
}
|
||
|
return dup;
|
||
|
}
|
||
|
_fullPath() {
|
||
|
let out = (this._path || '') + '/' + (this._childPath || '');
|
||
|
return out;
|
||
|
}
|
||
|
}
|
||
|
exports.DataSnapshot = DataSnapshot;
|