diff --git a/gulpfile.js b/gulpfile.js index 9ed848daf7..d123f14d81 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -54,6 +54,7 @@ var paths = { curatedTypings: [ 'src/*.d.ts', + '!src/database.d.ts', '!src/instance-id.d.ts', '!src/security-rules.d.ts', '!src/project-management.d.ts', @@ -68,7 +69,6 @@ const TEMPORARY_TYPING_EXCLUDES = [ '!lib/firebase-app.d.ts', '!lib/firebase-service.d.ts', '!lib/auth/*.d.ts', - '!lib/database/*.d.ts', '!lib/machine-learning/*.d.ts', '!lib/storage/*.d.ts', '!lib/utils/*.d.ts', diff --git a/package-lock.json b/package-lock.json index 623482b748..77678dded8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -156,15 +156,15 @@ } }, "@firebase/app": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.7.tgz", - "integrity": "sha512-6NpIZ3iMrCR2XOShK5oi3YYB0GXX5yxVD8p3+2N+X4CF5cERyIrDRf8+YXOFgr+bDHSbVcIyzpWv6ijhg4MJlw==", + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.9.tgz", + "integrity": "sha512-X2riRgK49IK8LCQ3j7BKLu3zqHDTJSaT6YgcLewtHuOVwtpHfGODiS1cL5VMvKm3ogxP84GA70tN3sdoL/vTog==", "dev": true, "requires": { "@firebase/app-types": "0.6.1", - "@firebase/component": "0.1.15", - "@firebase/logger": "0.2.5", - "@firebase/util": "0.2.50", + "@firebase/component": "0.1.17", + "@firebase/logger": "0.2.6", + "@firebase/util": "0.3.0", "dom-storage": "2.1.0", "tslib": "^1.11.1", "xmlhttprequest": "1.8.0" @@ -176,12 +176,20 @@ "integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg==" }, "@firebase/auth": { - "version": "0.13.6", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.13.6.tgz", - "integrity": "sha512-ERlda/t5RimNw5Err+5HJATC/qFkC64zR40G+4nK5b9eFJEm0MB+/DaismCwp6J6GoVL3NmejoVbuWU7sV4G1w==", + "version": "0.14.9", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.14.9.tgz", + "integrity": "sha512-PxYa2r5qUEdheXTvqROFrMstK8W4uPiP7NVfp+2Bec+AjY5PxZapCx/YFDLkU0D7YBI82H74PtZrzdJZw7TJ4w==", "dev": true, "requires": { - "@firebase/auth-types": "0.9.6" + "@firebase/auth-types": "0.10.1" + }, + "dependencies": { + "@firebase/auth-types": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.10.1.tgz", + "integrity": "sha512-/+gBHb1O9x/YlG7inXfxff/6X3BPZt4zgBv4kql6HEmdzNQCodIRlEYnI+/da+lN+dha7PjaFH7C7ewMmfV7rw==", + "dev": true + } } }, "@firebase/auth-interop-types": { @@ -190,51 +198,78 @@ "integrity": "sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw==" }, "@firebase/auth-types": { - "version": "0.9.6", - "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.9.6.tgz", - "integrity": "sha512-HB1yXe5hgiwPMukLBEfC3TQX22U9qKczj8kEclKhL7rnds3FKZWMM0+EpKbcJREbU9Sj/rgwgaio7ovSN4ZQFA==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.10.1.tgz", + "integrity": "sha512-/+gBHb1O9x/YlG7inXfxff/6X3BPZt4zgBv4kql6HEmdzNQCodIRlEYnI+/da+lN+dha7PjaFH7C7ewMmfV7rw==", "dev": true }, "@firebase/component": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.15.tgz", - "integrity": "sha512-HqFb1qQl1vtlUMIzPM15plNz27jqM8DWjuQQuGeDfG+4iRRflwKfgNw1BOyoP4kQ8vOBCL7t/71yPXSomNdJdQ==", + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.17.tgz", + "integrity": "sha512-/tN5iLcFp9rdpTfCJPfQ/o2ziGHlDxOzNx6XD2FoHlu4pG/PPGu+59iRfQXIowBGhxcTGD/l7oJhZEY/PVg0KQ==", + "dev": true, "requires": { - "@firebase/util": "0.2.50", + "@firebase/util": "0.3.0", "tslib": "^1.11.1" } }, "@firebase/database": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.6.6.tgz", - "integrity": "sha512-TqUJOaCATF/h3wpqhPT9Fz1nZI6gBv/M2pHZztUjX4A9o9Bq93NyqUurYiZnGB7zpSkEADFCVT4f0VBrWdHlNw==", + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.6.10.tgz", + "integrity": "sha512-Hc8zIPAroIbAoRe6xFCI5oFHubcHKoDsbYE3J5G1/BhT6DnEUSoLgx8kJ2npybVSCVyb8BvsD6swh17DGEz+0g==", "requires": { "@firebase/auth-interop-types": "0.1.5", - "@firebase/component": "0.1.15", - "@firebase/database-types": "0.5.1", - "@firebase/logger": "0.2.5", - "@firebase/util": "0.2.50", + "@firebase/component": "0.1.17", + "@firebase/database-types": "0.5.2", + "@firebase/logger": "0.2.6", + "@firebase/util": "0.3.0", "faye-websocket": "0.11.3", "tslib": "^1.11.1" + }, + "dependencies": { + "@firebase/component": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.17.tgz", + "integrity": "sha512-/tN5iLcFp9rdpTfCJPfQ/o2ziGHlDxOzNx6XD2FoHlu4pG/PPGu+59iRfQXIowBGhxcTGD/l7oJhZEY/PVg0KQ==", + "requires": { + "@firebase/util": "0.3.0", + "tslib": "^1.11.1" + } + }, + "@firebase/logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", + "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==" + }, + "@firebase/util": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.0.tgz", + "integrity": "sha512-GTwC+FSLeCPc44/TXCDReNQ5FPRIS5cb8Gr1XcD1TgiNBOvmyx61Om2YLwHp2GnN++6m6xmwmXARm06HOukATA==", + "requires": { + "tslib": "^1.11.1" + } + } } }, "@firebase/database-types": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.5.1.tgz", - "integrity": "sha512-onQxom1ZBYBJ648w/VNRzUewovEDAH7lvnrrpCd69ukkyrMk6rGEO/PQ9BcNEbhlNtukpsqRS0oNOFlHs0FaSA==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.5.2.tgz", + "integrity": "sha512-ap2WQOS3LKmGuVFKUghFft7RxXTyZTDr0Xd8y2aqmWsbJVjgozi0huL/EUMgTjGFrATAjcf2A7aNs8AKKZ2a8g==", "requires": { "@firebase/app-types": "0.6.1" } }, "@firebase/logger": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.5.tgz", - "integrity": "sha512-qqw3m0tWs/qrg7axTZG/QZq24DIMdSY6dGoWuBn08ddq7+GLF5HiqkRj71XznYeUUbfRq5W9C/PSFnN4JxX+WA==" + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", + "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==", + "dev": true }, "@firebase/util": { - "version": "0.2.50", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.2.50.tgz", - "integrity": "sha512-vFE6+Jfc25u0ViSpFxxq0q5s+XmuJ/y7CL3ud79RQe+WLFFg+j0eH1t23k0yNSG9vZNM7h3uHRIXbV97sYLAyw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.0.tgz", + "integrity": "sha512-GTwC+FSLeCPc44/TXCDReNQ5FPRIS5cb8Gr1XcD1TgiNBOvmyx61Om2YLwHp2GnN++6m6xmwmXARm06HOukATA==", + "dev": true, "requires": { "tslib": "^1.11.1" } @@ -572,7 +607,8 @@ "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true }, "@types/minimist": { "version": "1.2.0", @@ -794,6 +830,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, "requires": { "ansi-wrap": "^0.1.0" } @@ -839,7 +876,8 @@ "ansi-wrap": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", - "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=" + "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", + "dev": true }, "any-promise": { "version": "1.3.0", @@ -909,7 +947,8 @@ "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true }, "arr-filter": { "version": "1.1.2", @@ -938,7 +977,8 @@ "arr-union": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true }, "array-differ": { "version": "1.0.0", @@ -1068,7 +1108,8 @@ "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true }, "astral-regex": { "version": "1.0.0", @@ -1156,7 +1197,8 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "base": { "version": "0.11.2", @@ -1281,6 +1323,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1715,7 +1758,8 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "concat-stream": { "version": "2.0.0", @@ -2699,6 +2743,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, "requires": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" @@ -2708,6 +2753,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, "requires": { "is-plain-object": "^2.0.4" } @@ -3524,6 +3570,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/gulp-filter/-/gulp-filter-6.0.0.tgz", "integrity": "sha512-veQFW93kf6jBdWdF/RxMEIlDK2mkjHyPftM381DID2C9ImTVngwYpyyThxm4/EpgcNOT37BLefzMOjEKbyYg0Q==", + "dev": true, "requires": { "multimatch": "^4.0.0", "plugin-error": "^1.0.1", @@ -4346,6 +4393,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, "requires": { "isobject": "^3.0.1" } @@ -4435,7 +4483,8 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true }, "isstream": { "version": "0.1.2", @@ -5244,6 +5293,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5386,6 +5436,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-4.0.0.tgz", "integrity": "sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ==", + "dev": true, "requires": { "@types/minimatch": "^3.0.3", "array-differ": "^3.0.0", @@ -5397,12 +5448,14 @@ "array-differ": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", - "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==" + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "dev": true }, "array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true } } }, @@ -6353,6 +6406,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "dev": true, "requires": { "ansi-colors": "^1.0.1", "arr-diff": "^4.0.0", @@ -7415,6 +7469,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/streamfilter/-/streamfilter-3.0.0.tgz", "integrity": "sha512-kvKNfXCmUyC8lAXSSHCIXBUlo/lhsLcCU/OmzACZYpRUdtKIH68xYhm/+HI15jFJYtNJGYtCgn2wmIiExY1VwA==", + "dev": true, "requires": { "readable-stream": "^3.0.6" }, @@ -7423,6 +7478,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", diff --git a/package.json b/package.json index 81008d942b..41f7b63281 100644 --- a/package.json +++ b/package.json @@ -54,10 +54,10 @@ ], "types": "./lib/index.d.ts", "dependencies": { - "@firebase/database": "^0.6.0", + "@firebase/database": "^0.6.10", + "@firebase/database-types": "^0.5.2", "@types/node": "^10.10.0", "dicer": "^0.3.0", - "gulp-filter": "^6.0.0", "jsonwebtoken": "^8.5.1", "node-forge": "^0.9.1" }, @@ -66,9 +66,9 @@ "@google-cloud/storage": "^5.0.0" }, "devDependencies": { - "@firebase/app": "^0.6.1", - "@firebase/auth": "^0.13.3", - "@firebase/auth-types": "^0.9.3", + "@firebase/app": "^0.6.9", + "@firebase/auth": "^0.14.9", + "@firebase/auth-types": "^0.10.1", "@types/bcrypt": "^2.0.0", "@types/chai": "^4.0.0", "@types/chai-as-promised": "^7.1.0", @@ -93,6 +93,7 @@ "eslint": "^6.8.0", "firebase-token-generator": "^2.0.0", "gulp": "^4.0.2", + "gulp-filter": "^6.0.0", "gulp-header": "^1.8.8", "gulp-replace": "^0.5.4", "gulp-typescript": "^5.0.1", diff --git a/src/database/database-internal.ts b/src/database/database-internal.ts new file mode 100644 index 0000000000..fa955a9b7e --- /dev/null +++ b/src/database/database-internal.ts @@ -0,0 +1,241 @@ +/*! + * 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. + */ + +import { URL } from 'url'; +import * as path from 'path'; + +import { FirebaseApp } from '../firebase-app'; +import { FirebaseDatabaseError, AppErrorCodes, FirebaseAppError } from '../utils/error'; +import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; +import { Database as DatabaseImpl } from '@firebase/database'; +import { Database } from './database'; + +import * as validator from '../utils/validator'; +import { AuthorizedHttpClient, HttpRequestConfig, HttpError } from '../utils/api-request'; +import { getSdkVersion } from '../utils/index'; + +/** + * Internals of a Database instance. + */ +class DatabaseInternals implements FirebaseServiceInternalsInterface { + + public databases: { + [dbUrl: string]: Database; + } = {}; + + /** + * Deletes the service and its associated resources. + * + * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. + */ + public delete(): Promise { + for (const dbUrl of Object.keys(this.databases)) { + const db: DatabaseImpl = ((this.databases[dbUrl] as any) as DatabaseImpl); + db.INTERNAL.delete(); + } + return Promise.resolve(undefined); + } +} + +export class DatabaseService implements FirebaseServiceInterface { + + public readonly INTERNAL: DatabaseInternals = new DatabaseInternals(); + + private readonly appInternal: FirebaseApp; + + constructor(app: FirebaseApp) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseDatabaseError({ + code: 'invalid-argument', + message: 'First argument passed to admin.database() must be a valid Firebase app instance.', + }); + } + this.appInternal = app; + } + + /** + * Returns the app associated with this DatabaseService instance. + * + * @return {FirebaseApp} The app associated with this DatabaseService instance. + */ + get app(): FirebaseApp { + return this.appInternal; + } + + public getDatabase(url?: string): Database { + const dbUrl: string = this.ensureUrl(url); + if (!validator.isNonEmptyString(dbUrl)) { + throw new FirebaseDatabaseError({ + code: 'invalid-argument', + message: 'Database URL must be a valid, non-empty URL string.', + }); + } + + let db: Database = this.INTERNAL.databases[dbUrl]; + if (typeof db === 'undefined') { + const rtdb = require('@firebase/database'); // eslint-disable-line @typescript-eslint/no-var-requires + db = rtdb.initStandalone(this.appInternal, dbUrl, getSdkVersion()).instance; + + const rulesClient = new DatabaseRulesClient(this.app, dbUrl); + db.getRules = () => { + return rulesClient.getRules(); + }; + db.getRulesJSON = () => { + return rulesClient.getRulesJSON(); + }; + db.setRules = (source: string) => { + return rulesClient.setRules(source); + }; + + this.INTERNAL.databases[dbUrl] = db; + } + return db; + } + + private ensureUrl(url?: string): string { + if (typeof url !== 'undefined') { + return url; + } else if (typeof this.appInternal.options.databaseURL !== 'undefined') { + return this.appInternal.options.databaseURL; + } + throw new FirebaseDatabaseError({ + code: 'invalid-argument', + message: 'Can\'t determine Firebase Database URL.', + }); + } +} + +const RULES_URL_PATH = '.settings/rules.json'; + +/** + * A helper client for managing RTDB security rules. + */ +class DatabaseRulesClient { + + private readonly dbUrl: string; + private readonly httpClient: AuthorizedHttpClient; + + constructor(app: FirebaseApp, dbUrl: string) { + const parsedUrl = new URL(dbUrl); + parsedUrl.pathname = path.join(parsedUrl.pathname, RULES_URL_PATH); + this.dbUrl = parsedUrl.toString(); + this.httpClient = new AuthorizedHttpClient(app); + } + + /** + * Gets the currently applied security rules as a string. The return value consists of + * the rules source including comments. + * + * @return {Promise} A promise fulfilled with the rules as a raw string. + */ + public getRules(): Promise { + const req: HttpRequestConfig = { + method: 'GET', + url: this.dbUrl, + }; + return this.httpClient.send(req) + .then((resp) => { + if (!resp.text) { + throw new FirebaseAppError(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. + * + * @return {Promise} A promise fulfilled with the parsed rules source. + */ + public getRulesJSON(): Promise { + const req: HttpRequestConfig = { + 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. + * @return {Promise} Resolves when the rules are set on the Database. + */ + public setRules(source: string | Buffer | object): Promise { + if (!validator.isNonEmptyString(source) && + !validator.isBuffer(source) && + !validator.isNonNullObject(source)) { + const error = new FirebaseDatabaseError({ + code: 'invalid-argument', + message: 'Source must be a non-empty string, Buffer or an object.', + }); + return Promise.reject(error); + } + + const req: HttpRequestConfig = { + 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); + }); + } + + private handleError(err: Error): Error { + if (err instanceof HttpError) { + return new FirebaseDatabaseError({ + code: AppErrorCodes.INTERNAL_ERROR, + message: this.getErrorMessage(err), + }); + } + return err; + } + + private getErrorMessage(err: HttpError): string { + const intro = 'Error while accessing security rules'; + try { + const body: { error?: string } = err.response.data; + if (body && body.error) { + return `${intro}: ${body.error.trim()}`; + } + } catch { + // Ignore parsing errors + } + + return `${intro}: ${err.response.text}`; + } +} diff --git a/src/database/database.ts b/src/database/database.ts index 686120909d..474426748f 100644 --- a/src/database/database.ts +++ b/src/database/database.ts @@ -1,233 +1,49 @@ -import { URL } from 'url'; -import * as path from 'path'; - -import { FirebaseApp } from '../firebase-app'; -import { FirebaseDatabaseError, AppErrorCodes, FirebaseAppError } from '../utils/error'; -import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; -import { Database } from '@firebase/database'; - -import * as validator from '../utils/validator'; -import { AuthorizedHttpClient, HttpRequestConfig, HttpError } from '../utils/api-request'; -import { getSdkVersion } from '../utils/index'; - - -/** - * Internals of a Database instance. +/*! + * 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. */ -class DatabaseInternals implements FirebaseServiceInternalsInterface { - - public databases: { - [dbUrl: string]: Database; - } = {}; - /** - * Deletes the service and its associated resources. - * - * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. - */ - public delete(): Promise { - for (const dbUrl of Object.keys(this.databases)) { - const db: Database = this.databases[dbUrl]; - db.INTERNAL.delete(); - } - return Promise.resolve(undefined); - } -} - -declare module '@firebase/database' { - interface Database { +// Required to perform module augmentation to FirebaseDatabase interface. +import { FirebaseDatabase } from '@firebase/database-types'; + +declare module '@firebase/database-types' { + interface FirebaseDatabase { + /** + * Gets the currently applied security rules as a string. The return value consists of + * the rules source including comments. + * + * @return A promise fulfilled with the rules as a raw string. + */ getRules(): Promise; - getRulesJSON(): Promise; - setRules(source: string | Buffer | object): Promise; - } -} - -export class DatabaseService implements FirebaseServiceInterface { - - public readonly INTERNAL: DatabaseInternals = new DatabaseInternals(); - - private readonly appInternal: FirebaseApp; - - constructor(app: FirebaseApp) { - if (!validator.isNonNullObject(app) || !('options' in app)) { - throw new FirebaseDatabaseError({ - code: 'invalid-argument', - message: 'First argument passed to admin.database() must be a valid Firebase app instance.', - }); - } - this.appInternal = app; - } - - /** - * Returns the app associated with this DatabaseService instance. - * - * @return {FirebaseApp} The app associated with this DatabaseService instance. - */ - get app(): FirebaseApp { - return this.appInternal; - } - - public getDatabase(url?: string): Database { - const dbUrl: string = this.ensureUrl(url); - if (!validator.isNonEmptyString(dbUrl)) { - throw new FirebaseDatabaseError({ - code: 'invalid-argument', - message: 'Database URL must be a valid, non-empty URL string.', - }); - } - - let db: Database = this.INTERNAL.databases[dbUrl]; - if (typeof db === 'undefined') { - const rtdb = require('@firebase/database'); // eslint-disable-line @typescript-eslint/no-var-requires - db = rtdb.initStandalone(this.appInternal, dbUrl, 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.INTERNAL.databases[dbUrl] = db; - } - return db; - } + /** + * Gets the currently applied security rules as a parsed JSON object. Any comments in + * the original source are stripped away. + * + * @return A promise fulfilled with the parsed rules object. + */ + getRulesJSON(): Promise; - private ensureUrl(url?: string): string { - if (typeof url !== 'undefined') { - return url; - } else if (typeof this.appInternal.options.databaseURL !== 'undefined') { - return this.appInternal.options.databaseURL; - } - throw new FirebaseDatabaseError({ - code: 'invalid-argument', - message: 'Can\'t determine Firebase Database URL.', - }); + /** + * Sets the specified rules on the Firebase Realtime Database instance. If the rules source is + * specified as a string or a Buffer, it may include comments. + * + * @param source Source of the rules to apply. Must not be `null` or empty. + * @return Resolves when the rules are set on the Realtime Database. + */ + setRules(source: string | Buffer | object): Promise; } } -const RULES_URL_PATH = '.settings/rules.json'; - -/** - * A helper client for managing RTDB security rules. - */ -class DatabaseRulesClient { - - private readonly dbUrl: string; - private readonly httpClient: AuthorizedHttpClient; - - constructor(app: FirebaseApp, dbUrl: string) { - const parsedUrl = new URL(dbUrl); - parsedUrl.pathname = path.join(parsedUrl.pathname, RULES_URL_PATH); - this.dbUrl = parsedUrl.toString(); - this.httpClient = new AuthorizedHttpClient(app); - } - - /** - * Gets the currently applied security rules as a string. The return value consists of - * the rules source including comments. - * - * @return {Promise} A promise fulfilled with the rules as a raw string. - */ - public getRules(): Promise { - const req: HttpRequestConfig = { - method: 'GET', - url: this.dbUrl, - }; - return this.httpClient.send(req) - .then((resp) => { - if (!resp.text) { - throw new FirebaseAppError(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. - * - * @return {Promise} A promise fulfilled with the parsed rules source. - */ - public getRulesJSON(): Promise { - const req: HttpRequestConfig = { - 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. - * @return {Promise} Resolves when the rules are set on the Database. - */ - public setRules(source: string | Buffer | object): Promise { - if (!validator.isNonEmptyString(source) && - !validator.isBuffer(source) && - !validator.isNonNullObject(source)) { - const error = new FirebaseDatabaseError({ - code: 'invalid-argument', - message: 'Source must be a non-empty string, Buffer or an object.', - }); - return Promise.reject(error); - } - - const req: HttpRequestConfig = { - 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); - }); - } - - private handleError(err: Error): Error { - if (err instanceof HttpError) { - return new FirebaseDatabaseError({ - code: AppErrorCodes.INTERNAL_ERROR, - message: this.getErrorMessage(err), - }); - } - return err; - } - - private getErrorMessage(err: HttpError): string { - const intro = 'Error while accessing security rules'; - try { - const body: {error?: string} = err.response.data; - if (body && body.error) { - return `${intro}: ${body.error.trim()}`; - } - } catch { - // Ignore parsing errors - } - - return `${intro}: ${err.response.text}`; - } -} +export { FirebaseDatabase as Database } diff --git a/src/database/index.ts b/src/database/index.ts new file mode 100644 index 0000000000..30c6b1143a --- /dev/null +++ b/src/database/index.ts @@ -0,0 +1,52 @@ +/*! + * 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. + */ + +import { FirebaseApp } from '../firebase-app'; +import { ServerValue as sv } from '@firebase/database'; +import * as adminDb from './database'; +import * as firebaseDbTypesApi from '@firebase/database-types'; +import * as firebaseAdmin from '../index'; + +export function database(app?: FirebaseApp): adminDb.Database { + if (typeof(app) === 'undefined') { + app = firebaseAdmin.app(); + } + return app.database(); +} + +/** + * We must define a namespace to make the typings work correctly. Otherwise + * `admin.database()` cannot be called like a function. Temporarily, + * admin.database is used as the namespace name because we cannot barrel + * re-export the contents from @firebase/database-types, and we want it to + * match the namespacing in the re-export inside src/index.d.ts + */ +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace admin.database { + // See https://github.com/microsoft/TypeScript/issues/4336 + /* eslint-disable @typescript-eslint/no-unused-vars */ + // See https://github.com/typescript-eslint/typescript-eslint/issues/363 + export import DataSnapshot = firebaseDbTypesApi.DataSnapshot; + export import Database = adminDb.Database; + export import EventType = firebaseDbTypesApi.EventType; + export import OnDisconnect = firebaseDbTypesApi.OnDisconnect; + export import Query = firebaseDbTypesApi.Query; + export import Reference = firebaseDbTypesApi.Reference; + export import ThenableReference = firebaseDbTypesApi.ThenableReference; + export import enableLogging = firebaseDbTypesApi.enableLogging; + + export const ServerValue: firebaseDbTypesApi.ServerValue = sv; +} diff --git a/src/firebase-app.ts b/src/firebase-app.ts index 194b792ce4..fd8bd0f8d3 100644 --- a/src/firebase-app.ts +++ b/src/firebase-app.ts @@ -25,8 +25,8 @@ import { Auth } from './auth/auth'; import { MachineLearning } from './machine-learning/machine-learning'; import { Messaging } from './messaging/messaging'; import { Storage } from './storage/storage'; -import { Database } from '@firebase/database'; -import { DatabaseService } from './database/database'; +import { Database } from './database/database'; +import { DatabaseService } from './database/database-internal'; import { Firestore } from '@google-cloud/firestore'; import { FirestoreService } from './firestore/firestore-internal'; import { InstanceId } from './instance-id/instance-id'; @@ -307,7 +307,7 @@ export class FirebaseApp { */ public database(url?: string): Database { const service: DatabaseService = this.ensureService_('database', () => { - const dbService: typeof DatabaseService = require('./database/database').DatabaseService; + const dbService: typeof DatabaseService = require('./database/database-internal').DatabaseService; return new dbService(this); }); return service.getDatabase(url); diff --git a/src/firebase-namespace.ts b/src/firebase-namespace.ts index 916132ebbf..b81b3c6b91 100644 --- a/src/firebase-namespace.ts +++ b/src/firebase-namespace.ts @@ -31,7 +31,7 @@ import { Auth } from './auth/auth'; import { MachineLearning } from './machine-learning/machine-learning'; import { Messaging } from './messaging/messaging'; import { Storage } from './storage/storage'; -import { Database } from '@firebase/database'; +import { Database } from './database/database'; import { Firestore } from '@google-cloud/firestore'; import { InstanceId } from './instance-id/instance-id'; import { ProjectManagement } from './project-management/project-management'; diff --git a/test/integration/app.spec.ts b/test/integration/app.spec.ts index 3cdf356245..cce88a52aa 100644 --- a/test/integration/app.spec.ts +++ b/test/integration/app.spec.ts @@ -27,6 +27,20 @@ describe('admin', () => { expect(storageBucket).to.be.not.empty; }); + it('does not load RTDB by default', () => { + const firebaseRtdb = require.cache[require.resolve('@firebase/database')]; + expect(firebaseRtdb).to.be.undefined; + const rtdbInternal = require.cache[require.resolve('../../lib/database/database-internal')]; + expect(rtdbInternal).to.be.undefined; + }); + + it('loads RTDB when calling admin.database', () => { + const rtdbNamespace = admin.database; + expect(rtdbNamespace).to.not.be.null; + const firebaseRtdb = require.cache[require.resolve('@firebase/database')]; + expect(firebaseRtdb).to.not.be.undefined; + }); + it('does not load Firestore by default', () => { const gcloud = require.cache[require.resolve('@google-cloud/firestore')]; expect(gcloud).to.be.undefined; diff --git a/test/unit/database/database.spec.ts b/test/unit/database/database.spec.ts index 78d73f21d6..415ce4a94a 100644 --- a/test/unit/database/database.spec.ts +++ b/test/unit/database/database.spec.ts @@ -22,8 +22,8 @@ import * as sinon from 'sinon'; import * as mocks from '../../resources/mocks'; import { FirebaseApp } from '../../../src/firebase-app'; -import { DatabaseService } from '../../../src/database/database'; -import { Database } from '@firebase/database'; +import { DatabaseService } from '../../../src/database/database-internal'; +import { Database } from '../../../src/database/database'; import * as utils from '../utils'; import { HttpClient, HttpRequestConfig } from '../../../src/utils/api-request'; diff --git a/test/unit/firebase-app.spec.ts b/test/unit/firebase-app.spec.ts index dc89b6383c..2b3b9591d8 100644 --- a/test/unit/firebase-app.spec.ts +++ b/test/unit/firebase-app.spec.ts @@ -35,7 +35,7 @@ import { Messaging } from '../../src/messaging/messaging'; import { MachineLearning } from '../../src/machine-learning/machine-learning'; import { Storage } from '../../src/storage/storage'; import { Firestore } from '@google-cloud/firestore'; -import { Database } from '@firebase/database'; +import { Database } from '../../src/database/database'; import { InstanceId } from '../../src/instance-id/instance-id'; import { ProjectManagement } from '../../src/project-management/project-management'; import { SecurityRules } from '../../src/security-rules/security-rules'; diff --git a/test/unit/firebase-namespace.spec.ts b/test/unit/firebase-namespace.spec.ts index 60b26ff91d..4cbc98df5e 100644 --- a/test/unit/firebase-namespace.spec.ts +++ b/test/unit/firebase-namespace.spec.ts @@ -36,6 +36,7 @@ import { Reference, ServerValue, } from '@firebase/database'; +import { Database as FirebaseDatabase } from '../../src/database/database'; import { Messaging } from '../../src/messaging/messaging'; import { MachineLearning } from '../../src/machine-learning/machine-learning'; import { Storage } from '../../src/storage/storage'; @@ -404,14 +405,14 @@ describe('FirebaseNamespace', () => { it('should return a valid namespace when the default app is initialized', () => { const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); - const db: Database = firebaseNamespace.database(); + const db: FirebaseDatabase = firebaseNamespace.database(); expect(db.app).to.be.deep.equal(app); return app.delete(); }); it('should return a valid namespace when the named app is initialized', () => { const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); - const db: Database = firebaseNamespace.database(app); + const db: FirebaseDatabase = firebaseNamespace.database(app); expect(db.app).to.be.deep.equal(app); return app.delete(); });