diff --git a/packages/database/index.node.ts b/packages/database/index.node.ts index 50337ae756b..77ec2afa615 100644 --- a/packages/database/index.node.ts +++ b/packages/database/index.node.ts @@ -16,7 +16,7 @@ */ import { FirebaseNamespace, FirebaseApp } from '@firebase/app-types'; -import { _FirebaseNamespace } from '@firebase/app-types/private'; +import { _FirebaseNamespace, _FirebaseApp } from '@firebase/app-types/private'; import { Database } from './src/api/Database'; import { DataSnapshot } from './src/api/DataSnapshot'; import { Query } from './src/api/Query'; @@ -30,6 +30,13 @@ import { setSDKVersion } from './src/core/version'; import { CONSTANTS, isNodeSdk } from '@firebase/util'; import { setWebSocketImpl } from './src/realtime/WebSocketConnection'; import { Client } from 'faye-websocket'; +import { + Component, + ComponentType, + Provider, + ComponentContainer +} from '@firebase/component'; +import { FirebaseAuthInternal } from '@firebase/auth-interop-types'; setWebSocketImpl(Client); @@ -51,8 +58,28 @@ export function initStandalone(app: FirebaseApp, url: string, version: string) { CONSTANTS.NODE_ADMIN = true; setSDKVersion(version); + /** + * Create a 'auth-internal' component using firebase-admin-node's implementation + * that implements FirebaseAuthInternal. + * ComponentContainer('database-admin') is just a placeholder that doesn't perform + * any actual function. + */ + const authProvider = new Provider( + 'auth-internal', + new ComponentContainer('database-admin') + ); + authProvider.setComponent( + new Component( + 'auth-internal', + // firebase-admin-node's app.INTERNAL implements FirebaseAuthInternal interface + // eslint-disable-next-line @eslint-tslint/no-explicit-any + () => (app as any).INTERNAL, + ComponentType.PRIVATE + ) + ); + return { - instance: RepoManager.getInstance().databaseFromApp(app, url), + instance: RepoManager.getInstance().databaseFromApp(app, authProvider, url), namespace: { Reference, Query, @@ -71,22 +98,37 @@ export function registerDatabase(instance: FirebaseNamespace) { setSDKVersion(instance.SDK_VERSION); // Register the Database Service with the 'firebase' namespace. - const namespace = (instance as _FirebaseNamespace).INTERNAL.registerService( - 'database', - (app, unused, url) => RepoManager.getInstance().databaseFromApp(app, url), - // firebase.database namespace properties - { - Reference, - Query, - Database, - DataSnapshot, - enableLogging, - INTERNAL, - ServerValue, - TEST_ACCESS - }, - null, - true + const namespace = (instance as _FirebaseNamespace).INTERNAL.registerComponent( + new Component( + 'database', + (container, url) => { + /* Dependencies */ + // getImmediate for FirebaseApp will always succeed + const app = container.getProvider('app').getImmediate(); + const authProvider = container.getProvider('auth-internal'); + + return RepoManager.getInstance().databaseFromApp( + app, + authProvider, + url + ); + }, + ComponentType.PUBLIC + ) + .setServiceProps( + // firebase.database namespace properties + { + Reference, + Query, + Database, + DataSnapshot, + enableLogging, + INTERNAL, + ServerValue, + TEST_ACCESS + } + ) + .setMultipleInstances(true) ); if (isNodeSdk()) { diff --git a/packages/database/index.ts b/packages/database/index.ts index 00a3848447c..4b18d8f4fa2 100644 --- a/packages/database/index.ts +++ b/packages/database/index.ts @@ -29,6 +29,7 @@ import * as TEST_ACCESS from './src/api/test_access'; import { isNodeSdk } from '@firebase/util'; import * as types from '@firebase/database-types'; import { setSDKVersion } from './src/core/version'; +import { Component, ComponentType } from '@firebase/component'; const ServerValue = Database.ServerValue; @@ -37,22 +38,37 @@ export function registerDatabase(instance: FirebaseNamespace) { setSDKVersion(instance.SDK_VERSION); // Register the Database Service with the 'firebase' namespace. - const namespace = (instance as _FirebaseNamespace).INTERNAL.registerService( - 'database', - (app, unused, url) => RepoManager.getInstance().databaseFromApp(app, url), - // firebase.database namespace properties - { - Reference, - Query, - Database, - DataSnapshot, - enableLogging, - INTERNAL, - ServerValue, - TEST_ACCESS - }, - null, - true + const namespace = (instance as _FirebaseNamespace).INTERNAL.registerComponent( + new Component( + 'database', + (container, url) => { + /* Dependencies */ + // getImmediate for FirebaseApp will always succeed + const app = container.getProvider('app').getImmediate(); + const authProvider = container.getProvider('auth-internal'); + + return RepoManager.getInstance().databaseFromApp( + app, + authProvider, + url + ); + }, + ComponentType.PUBLIC + ) + .setServiceProps( + // firebase.database namespace properties + { + Reference, + Query, + Database, + DataSnapshot, + enableLogging, + INTERNAL, + ServerValue, + TEST_ACCESS + } + ) + .setMultipleInstances(true) ); if (isNodeSdk()) { diff --git a/packages/database/package.json b/packages/database/package.json index 634702eed86..d118ae93d0a 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -16,7 +16,7 @@ "test": "yarn test:emulator", "test:all": "run-p test:browser test:node", "test:browser": "karma start --single-run", - "test:node": "TS_NODE_CACHE=NO TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha 'test/{,!(browser)/**/}*.test.ts' --file index.node.ts --opts ../../config/mocha.node.opts", + "test:node": "TS_NODE_FILES=true TS_NODE_CACHE=NO TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha 'test/{,!(browser)/**/}*.test.ts' --file index.node.ts --opts ../../config/mocha.node.opts", "test:emulator": "ts-node --compiler-options='{\"module\":\"commonjs\"}' ../../scripts/emulator-testing/database-test-runner.ts", "prepare": "yarn build" }, diff --git a/packages/database/src/core/AuthTokenProvider.ts b/packages/database/src/core/AuthTokenProvider.ts index b8973d31c84..2c9aada9fa9 100644 --- a/packages/database/src/core/AuthTokenProvider.ts +++ b/packages/database/src/core/AuthTokenProvider.ts @@ -15,48 +15,65 @@ * limitations under the License. */ -import { FirebaseApp } from '@firebase/app-types'; import { FirebaseAuthTokenData } from '@firebase/app-types/private'; +import { FirebaseAuthInternal } from '@firebase/auth-interop-types'; +import { Provider } from '@firebase/component'; import { log, warn } from './util/util'; +import { FirebaseApp } from '@firebase/app-types'; /** * Abstraction around FirebaseApp's token fetching capabilities. */ export class AuthTokenProvider { - /** - * @param {!FirebaseApp} app_ - */ - constructor(private app_: FirebaseApp) {} + private auth_: FirebaseAuthInternal | null = null; + constructor( + private app_: FirebaseApp, + private authProvider_: Provider + ) { + this.auth_ = authProvider_.getImmediate({ optional: true }); + if (!this.auth_) { + authProvider_.get().then(auth => (this.auth_ = auth)); + } + } /** * @param {boolean} forceRefresh * @return {!Promise} */ getToken(forceRefresh: boolean): Promise { - return this.app_['INTERNAL']['getToken'](forceRefresh).then( - null, - // .catch - function(error) { - // TODO: Need to figure out all the cases this is raised and whether - // this makes sense. - if (error && error.code === 'auth/token-not-initialized') { - log('Got auth/token-not-initialized error. Treating as null token.'); - return null; - } else { - return Promise.reject(error); - } + if (!this.auth_) { + return Promise.resolve(null); + } + + return this.auth_.getToken(forceRefresh).catch(function(error) { + // TODO: Need to figure out all the cases this is raised and whether + // this makes sense. + if (error && error.code === 'auth/token-not-initialized') { + log('Got auth/token-not-initialized error. Treating as null token.'); + return null; + } else { + return Promise.reject(error); } - ); + }); } addTokenChangeListener(listener: (token: string | null) => void) { // TODO: We might want to wrap the listener and call it with no args to // avoid a leaky abstraction, but that makes removing the listener harder. - this.app_['INTERNAL']['addAuthTokenListener'](listener); + if (this.auth_) { + this.auth_.addAuthTokenListener(listener); + } else { + setTimeout(() => listener(null), 0); + this.authProvider_ + .get() + .then(auth => auth.addAuthTokenListener(listener)); + } } removeTokenChangeListener(listener: (token: string | null) => void) { - this.app_['INTERNAL']['removeAuthTokenListener'](listener); + this.authProvider_ + .get() + .then(auth => auth.removeAuthTokenListener(listener)); } notifyForInvalidToken() { diff --git a/packages/database/src/core/Repo.ts b/packages/database/src/core/Repo.ts index 290e75d03c1..c198e6251a4 100644 --- a/packages/database/src/core/Repo.ts +++ b/packages/database/src/core/Repo.ts @@ -44,6 +44,8 @@ import { EventRegistration } from './view/EventRegistration'; import { StatsCollection } from './stats/StatsCollection'; import { Event } from './view/Event'; import { Node } from './snap/Node'; +import { FirebaseAuthInternal } from '@firebase/auth-interop-types'; +import { Provider } from '@firebase/component'; const INTERRUPT_REASON = 'repo_interrupt'; @@ -79,9 +81,10 @@ export class Repo { constructor( public repoInfo_: RepoInfo, forceRestClient: boolean, - public app: FirebaseApp + public app: FirebaseApp, + authProvider: Provider ) { - const authTokenProvider = new AuthTokenProvider(app); + const authTokenProvider = new AuthTokenProvider(app, authProvider); this.stats_ = StatsManager.getCollection(repoInfo_); diff --git a/packages/database/src/core/RepoManager.ts b/packages/database/src/core/RepoManager.ts index 7dee63393c7..a48a20b9d70 100644 --- a/packages/database/src/core/RepoManager.ts +++ b/packages/database/src/core/RepoManager.ts @@ -24,6 +24,8 @@ import { validateUrl } from './util/validation'; import './Repo_transaction'; import { Database } from '../api/Database'; import { RepoInfo } from './RepoInfo'; +import { FirebaseAuthInternal } from '@firebase/auth-interop-types'; +import { Provider } from '@firebase/component'; /** @const {string} */ const DATABASE_URL_OPTION = 'databaseURL'; @@ -89,7 +91,11 @@ export class RepoManager { * @param {!FirebaseApp} app * @return {!Database} */ - databaseFromApp(app: FirebaseApp, url?: string): Database { + databaseFromApp( + app: FirebaseApp, + authProvider: Provider, + url?: string + ): Database { let dbUrl: string | undefined = url || app.options[DATABASE_URL_OPTION]; if (dbUrl === undefined) { fatal( @@ -120,7 +126,7 @@ export class RepoManager { ); } - const repo = this.createRepo(repoInfo, app); + const repo = this.createRepo(repoInfo, app, authProvider); return repo.database; } @@ -150,7 +156,11 @@ export class RepoManager { * @param {!FirebaseApp} app * @return {!Repo} The Repo object for the specified server / repoName. */ - createRepo(repoInfo: RepoInfo, app: FirebaseApp): Repo { + createRepo( + repoInfo: RepoInfo, + app: FirebaseApp, + authProvider: Provider + ): Repo { let appRepos = safeGet(this.repos_, app.name); if (!appRepos) { @@ -164,7 +174,7 @@ export class RepoManager { 'Database initialized multiple times. Please make sure the format of the database URL matches with each database() call.' ); } - repo = new Repo(repoInfo, this.useRestClient_, app); + repo = new Repo(repoInfo, this.useRestClient_, app, authProvider); appRepos[repoInfo.toURLString()] = repo; return repo; diff --git a/packages/database/test/browser/crawler_support.test.ts b/packages/database/test/browser/crawler_support.test.ts index 8c2c8862dac..4d97ba1a968 100644 --- a/packages/database/test/browser/crawler_support.test.ts +++ b/packages/database/test/browser/crawler_support.test.ts @@ -18,18 +18,13 @@ import { expect } from 'chai'; import { forceRestClient } from '../../src/api/test_access'; -import { - getRandomNode, - testAuthTokenProvider, - getFreshRepoFromReference -} from '../helpers/util'; +import { getRandomNode, getFreshRepoFromReference } from '../helpers/util'; // Some sanity checks for the ReadonlyRestClient crawler support. describe('Crawler Support', function() { let initialData; let normalRef; let restRef; - let tokenProvider; beforeEach(function(done) { normalRef = getRandomNode(); @@ -38,15 +33,9 @@ describe('Crawler Support', function() { restRef = getFreshRepoFromReference(normalRef); forceRestClient(false); - tokenProvider = testAuthTokenProvider(restRef.database.app); - setInitialData(done); }); - afterEach(function() { - tokenProvider.setToken(null); - }); - function setInitialData(done) { // Set some initial data. initialData = { diff --git a/packages/database/test/helpers/util.ts b/packages/database/test/helpers/util.ts index b9016814ea4..b4327407e53 100644 --- a/packages/database/test/helpers/util.ts +++ b/packages/database/test/helpers/util.ts @@ -22,6 +22,8 @@ import '../../index'; import { Reference } from '../../src/api/Reference'; import { Query } from '../../src/api/Query'; import { ConnectionTarget } from '../../src/api/test_access'; +import { _FirebaseNamespace } from '@firebase/app-types/private'; +import { Component, ComponentType } from '@firebase/component'; export const TEST_PROJECT = require('../../../../config/project.json'); @@ -50,30 +52,21 @@ console.log(`USE_EMULATOR: ${USE_EMULATOR}. DATABASE_URL: ${DATABASE_URL}.`); let numDatabases = 0; -/** - * Fake Firebase App Authentication functions for testing. - * @param {!FirebaseApp} app - * @return {!FirebaseApp} - */ -export function patchFakeAuthFunctions(app) { - const token_ = null; - - app['INTERNAL'] = app['INTERNAL'] || {}; - - app['INTERNAL']['getToken'] = function(forceRefresh) { - return Promise.resolve(token_); - }; - - app['INTERNAL']['addAuthTokenListener'] = function(listener) {}; - - app['INTERNAL']['removeAuthTokenListener'] = function(listener) {}; - - return app; -} +// mock authentication functions for testing +(firebase as _FirebaseNamespace).INTERNAL.registerComponent( + new Component( + 'auth-internal', + () => ({ + getToken: () => Promise.resolve(null), + addAuthTokenListener: () => {}, + removeAuthTokenListener: () => {} + }), + ComponentType.PRIVATE + ) +); export function createTestApp() { const app = firebase.initializeApp({ databaseURL: DATABASE_URL }); - patchFakeAuthFunctions(app); return app; } @@ -94,7 +87,6 @@ export function getRootNode(i = 0, ref?: string) { app = firebase.app('TEST-' + i); } catch (e) { app = firebase.initializeApp({ databaseURL: DATABASE_URL }, 'TEST-' + i); - patchFakeAuthFunctions(app); } db = app.database(); return db.ref(ref); @@ -148,59 +140,6 @@ export function shuffle(arr, randFn = Math.random) { } } -export function testAuthTokenProvider(app) { - let token_ = null; - let nextToken_ = null; - let hasNextToken_ = false; - const listeners_ = []; - - app['INTERNAL'] = app['INTERNAL'] || {}; - - app['INTERNAL']['getToken'] = function(forceRefresh) { - if (forceRefresh && hasNextToken_) { - token_ = nextToken_; - hasNextToken_ = false; - } - return Promise.resolve({ accessToken: token_ }); - }; - - app['INTERNAL']['addAuthTokenListener'] = function(listener) { - const token = token_; - listeners_.push(listener); - const async = Promise.resolve(); - async.then(function() { - listener(token); - }); - }; - - app['INTERNAL']['removeAuthTokenListener'] = function(listener) { - throw Error('removeAuthTokenListener not supported in testing'); - }; - - return { - setToken: function(token) { - token_ = token; - const async = Promise.resolve(); - for (let i = 0; i < listeners_.length; i++) { - async.then( - (function(idx) { - return function() { - listeners_[idx](token); - }; - })(i) - ); - } - - // Any future thens are guaranteed to be resolved after the listeners have been notified - return async; - }, - setNextToken: function(token) { - nextToken_ = token; - hasNextToken_ = true; - } - }; -} - let freshRepoId = 1; const activeFreshApps = []; @@ -209,7 +148,6 @@ export function getFreshRepo(path) { { databaseURL: DATABASE_URL }, 'ISOLATED_REPO_' + freshRepoId++ ); - patchFakeAuthFunctions(app); activeFreshApps.push(app); return (app as any).database().ref(path); }