UNPKG

firebase-admin

Version:
258 lines (257 loc) 9.26 kB
/*! firebase-admin v13.2.0 */ "use strict"; /*! * Copyright 2020 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file 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. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.DatabaseService = void 0; const url_1 = require("url"); const path = require("path"); const error_1 = require("../utils/error"); const validator = require("../utils/validator"); const api_request_1 = require("../utils/api-request"); const index_1 = require("../utils/index"); const TOKEN_REFRESH_THRESHOLD_MILLIS = 5 * 60 * 1000; class DatabaseService { constructor(app) { this.databases = {}; if (!validator.isNonNullObject(app) || !('options' in app)) { throw new error_1.FirebaseDatabaseError({ code: 'invalid-argument', message: 'First argument passed to admin.database() must be a valid Firebase app instance.', }); } this.appInternal = app; } get firebaseApp() { return this.app; } /** * @internal */ delete() { if (this.tokenListener) { this.firebaseApp.INTERNAL.removeAuthTokenListener(this.tokenListener); clearTimeout(this.tokenRefreshTimeout); } const promises = []; for (const dbUrl of Object.keys(this.databases)) { const db = this.databases[dbUrl]; promises.push(db.INTERNAL.delete()); } return Promise.all(promises).then(() => { this.databases = {}; }); } /** * Returns the app associated with this DatabaseService instance. * * @returns The app associated with this DatabaseService instance. */ get app() { return this.appInternal; } getDatabase(url) { const dbUrl = this.ensureUrl(url); if (!validator.isNonEmptyString(dbUrl)) { throw new error_1.FirebaseDatabaseError({ code: 'invalid-argument', message: 'Database URL must be a valid, non-empty URL string.', }); } let db = this.databases[dbUrl]; if (typeof db === 'undefined') { // eslint-disable-next-line @typescript-eslint/no-var-requires const rtdb = require('@firebase/database-compat/standalone'); db = rtdb.initStandalone(this.appInternal, dbUrl, (0, index_1.getSdkVersion)()).instance; const rulesClient = new DatabaseRulesClient(this.app, dbUrl); db.getRules = () => { return rulesClient.getRules(); }; db.getRulesJSON = () => { return rulesClient.getRulesJSON(); }; db.setRules = (source) => { return rulesClient.setRules(source); }; this.databases[dbUrl] = db; } if (!this.tokenListener) { this.tokenListener = this.onTokenChange.bind(this); this.firebaseApp.INTERNAL.addAuthTokenListener(this.tokenListener); } return db; } // eslint-disable-next-line @typescript-eslint/no-unused-vars onTokenChange(_) { const token = this.firebaseApp.INTERNAL.getCachedToken(); if (token) { const delayMillis = token.expirationTime - TOKEN_REFRESH_THRESHOLD_MILLIS - Date.now(); // If the new token is set to expire soon (unlikely), do nothing. Somebody will eventually // notice and refresh the token, at which point this callback will fire again. if (delayMillis > 0) { this.scheduleTokenRefresh(delayMillis); } } } scheduleTokenRefresh(delayMillis) { clearTimeout(this.tokenRefreshTimeout); this.tokenRefreshTimeout = setTimeout(() => { this.firebaseApp.INTERNAL.getToken(/*forceRefresh=*/ true) .catch(() => { // Ignore the error since this might just be an intermittent failure. If we really cannot // refresh the token, an error will be logged once the existing token expires and we try // to fetch a fresh one. }); }, delayMillis); } ensureUrl(url) { if (typeof url !== 'undefined') { return url; } else if (typeof this.appInternal.options.databaseURL !== 'undefined') { return this.appInternal.options.databaseURL; } throw new error_1.FirebaseDatabaseError({ code: 'invalid-argument', message: 'Can\'t determine Firebase Database URL.', }); } } exports.DatabaseService = DatabaseService; const RULES_URL_PATH = '.settings/rules.json'; /** * A helper client for managing RTDB security rules. */ class DatabaseRulesClient { constructor(app, dbUrl) { let parsedUrl = new url_1.URL(dbUrl); const emulatorHost = process.env.FIREBASE_DATABASE_EMULATOR_HOST; if (emulatorHost) { const namespace = extractNamespace(parsedUrl); parsedUrl = new url_1.URL(`http://${emulatorHost}?ns=${namespace}`); } parsedUrl.pathname = path.join(parsedUrl.pathname, RULES_URL_PATH); this.dbUrl = parsedUrl.toString(); this.httpClient = new api_request_1.AuthorizedHttpClient(app); } /** * Gets the currently applied security rules as a string. The return value consists of * the rules source including comments. * * @returns A promise fulfilled with the rules as a raw string. */ getRules() { const req = { method: 'GET', url: this.dbUrl, }; return this.httpClient.send(req) .then((resp) => { if (!resp.text) { throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INTERNAL_ERROR, 'HTTP response missing data.'); } return resp.text; }) .catch((err) => { throw this.handleError(err); }); } /** * Gets the currently applied security rules as a parsed JSON object. Any comments in * the original source are stripped away. * * @returns {Promise<object>} A promise fulfilled with the parsed rules source. */ getRulesJSON() { const req = { method: 'GET', url: this.dbUrl, data: { format: 'strict' }, }; return this.httpClient.send(req) .then((resp) => { return resp.data; }) .catch((err) => { throw this.handleError(err); }); } /** * Sets the specified rules on the Firebase Database instance. If the rules source is * specified as a string or a Buffer, it may include comments. * * @param {string|Buffer|object} source Source of the rules to apply. Must not be `null` * or empty. * @returns {Promise<void>} Resolves when the rules are set on the Database. */ setRules(source) { if (!validator.isNonEmptyString(source) && !validator.isBuffer(source) && !validator.isNonNullObject(source)) { const error = new error_1.FirebaseDatabaseError({ code: 'invalid-argument', message: 'Source must be a non-empty string, Buffer or an object.', }); return Promise.reject(error); } const req = { method: 'PUT', url: this.dbUrl, data: source, headers: { 'content-type': 'application/json; charset=utf-8', }, }; return this.httpClient.send(req) .then(() => { return; }) .catch((err) => { throw this.handleError(err); }); } handleError(err) { if (err instanceof api_request_1.RequestResponseError) { return new error_1.FirebaseDatabaseError({ code: error_1.AppErrorCodes.INTERNAL_ERROR, message: this.getErrorMessage(err), }); } return err; } getErrorMessage(err) { const intro = 'Error while accessing security rules'; try { const body = err.response.data; if (body && body.error) { return `${intro}: ${body.error.trim()}`; } } catch { // Ignore parsing errors } return `${intro}: ${err.response.text}`; } } function extractNamespace(parsedUrl) { const ns = parsedUrl.searchParams.get('ns'); if (ns) { return ns; } const hostname = parsedUrl.hostname; const dotIndex = hostname.indexOf('.'); return hostname.substring(0, dotIndex).toLowerCase(); }