UNPKG

ran-boilerplate

Version:

React . Apollo (GraphQL) . Next.js Toolkit

287 lines (286 loc) 11 kB
"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;