Skip to content

Commit

Permalink
modular autoinit (#6526)
Browse files Browse the repository at this point in the history
Add functionality to read config from __FIREBASE_DEFAULTS__ global or env variable. See go/firebase-api-client-autoinit
  • Loading branch information
jamesdaniels committed Sep 28, 2022
1 parent ee871fc commit fdd4ab4
Show file tree
Hide file tree
Showing 17 changed files with 333 additions and 30 deletions.
13 changes: 13 additions & 0 deletions .changeset/mighty-insects-judge.md
@@ -0,0 +1,13 @@
---
'@firebase/app': minor
'@firebase/app-types': minor
'@firebase/util': minor
'@firebase/auth': patch
'@firebase/database': patch
'@firebase/firestore': patch
'@firebase/functions': patch
'@firebase/storage': patch
'firebase': minor
---

Add functionality to auto-initialize project config and emulator settings from global defaults provided by framework tooling.
3 changes: 3 additions & 0 deletions common/api-review/app.api.md
Expand Up @@ -93,6 +93,9 @@ export function initializeApp(options: FirebaseOptions, name?: string): Firebase
// @public
export function initializeApp(options: FirebaseOptions, config?: FirebaseAppSettings): FirebaseApp;

// @public
export function initializeApp(): FirebaseApp;

// @public
export function onLog(logCallback: LogCallback | null, options?: LogOptions): void;

Expand Down
4 changes: 2 additions & 2 deletions common/api-review/firestore.api.md
Expand Up @@ -262,10 +262,10 @@ export function getDocsFromCache<T>(query: Query<T>): Promise<QuerySnapshot<T>>;
export function getDocsFromServer<T>(query: Query<T>): Promise<QuerySnapshot<T>>;

// @public
export function getFirestore(): Firestore;
export function getFirestore(app: FirebaseApp): Firestore;

// @public
export function getFirestore(app: FirebaseApp): Firestore;
export function getFirestore(): Firestore;

// @public
export function increment(n: number): FieldValue;
Expand Down
26 changes: 26 additions & 0 deletions common/api-review/util.api.md
Expand Up @@ -173,11 +173,28 @@ export function errorPrefix(fnName: string, argName: string): string;
// @public (undocumented)
export type Executor<T> = (observer: Observer<T>) => void;

// @public
export type ExperimentalKey = 'authTokenSyncURL' | 'authIdTokenMaxAge';

// Warning: (ae-missing-release-tag) "extractQuerystring" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export function extractQuerystring(url: string): string;

// @public
export interface FirebaseDefaults {
// (undocumented)
[key: string]: unknown;
// (undocumented)
_authIdTokenMaxAge?: number;
// (undocumented)
_authTokenSyncURL?: string;
// (undocumented)
config?: Record<string, string>;
// (undocumented)
emulatorHosts?: Record<string, string>;
}

// Warning: (ae-missing-release-tag) "FirebaseError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
Expand All @@ -195,6 +212,15 @@ export class FirebaseError extends Error {
// @public
export type FirebaseSignInProvider = 'custom' | 'email' | 'password' | 'phone' | 'anonymous' | 'google.com' | 'facebook.com' | 'github.com' | 'twitter.com' | 'microsoft.com' | 'apple.com';

// @public
export const getDefaultAppConfig: () => Record<string, string> | undefined;

// @public
export const getDefaultEmulatorHost: (productName: string) => string | undefined;

// @public
export const getExperimentalSetting: <T extends ExperimentalKey>(name: T) => FirebaseDefaults[`_${T}`];

// Warning: (ae-missing-release-tag) "getGlobal" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
Expand Down
21 changes: 19 additions & 2 deletions packages/app/src/api.ts
Expand Up @@ -39,7 +39,7 @@ import {
LogOptions,
setUserLogHandler
} from '@firebase/logger';
import { deepEqual } from '@firebase/util';
import { deepEqual, getDefaultAppConfig } from '@firebase/util';

export { FirebaseError } from '@firebase/util';

Expand Down Expand Up @@ -110,10 +110,18 @@ export function initializeApp(
options: FirebaseOptions,
config?: FirebaseAppSettings
): FirebaseApp;
/**
* Creates and initializes a FirebaseApp instance.
*
* @public
*/
export function initializeApp(): FirebaseApp;
export function initializeApp(
options: FirebaseOptions,
_options?: FirebaseOptions,
rawConfig = {}
): FirebaseApp {
let options = _options;

if (typeof rawConfig !== 'object') {
const name = rawConfig;
rawConfig = { name };
Expand All @@ -132,6 +140,12 @@ export function initializeApp(
});
}

options ||= getDefaultAppConfig();

if (!options) {
throw ERROR_FACTORY.create(AppError.NO_OPTIONS);
}

const existingApp = _apps.get(name) as FirebaseAppImpl;
if (existingApp) {
// return the existing app if options and config deep equal the ones in the existing app.
Expand Down Expand Up @@ -188,6 +202,9 @@ export function initializeApp(
*/
export function getApp(name: string = DEFAULT_ENTRY_NAME): FirebaseApp {
const app = _apps.get(name);
if (!app && name === DEFAULT_ENTRY_NAME) {
return initializeApp();
}
if (!app) {
throw ERROR_FACTORY.create(AppError.NO_APP, { appName: name });
}
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/errors.ts
Expand Up @@ -22,6 +22,7 @@ export const enum AppError {
BAD_APP_NAME = 'bad-app-name',
DUPLICATE_APP = 'duplicate-app',
APP_DELETED = 'app-deleted',
NO_OPTIONS = 'no-options',
INVALID_APP_ARGUMENT = 'invalid-app-argument',
INVALID_LOG_ARGUMENT = 'invalid-log-argument',
IDB_OPEN = 'idb-open',
Expand All @@ -38,6 +39,8 @@ const ERRORS: ErrorMap<AppError> = {
[AppError.DUPLICATE_APP]:
"Firebase App named '{$appName}' already exists with different options or config",
[AppError.APP_DELETED]: "Firebase App named '{$appName}' already deleted",
[AppError.NO_OPTIONS]:
'Need to provide options, when not being deployed to hosting via source.',
[AppError.INVALID_APP_ARGUMENT]:
'firebase.{$appName}() takes either no argument or a ' +
'Firebase App instance.',
Expand Down
58 changes: 55 additions & 3 deletions packages/auth/src/platform_browser/index.ts
Expand Up @@ -17,14 +17,50 @@

import { FirebaseApp, getApp, _getProvider } from '@firebase/app';

import { initializeAuth } from '..';
import {
initializeAuth,
beforeAuthStateChanged,
onIdTokenChanged,
connectAuthEmulator
} from '..';
import { registerAuth } from '../core/auth/register';
import { ClientPlatform } from '../core/util/version';
import { browserLocalPersistence } from './persistence/local_storage';
import { browserSessionPersistence } from './persistence/session_storage';
import { indexedDBLocalPersistence } from './persistence/indexed_db';
import { browserPopupRedirectResolver } from './popup_redirect';
import { Auth } from '../model/public_types';
import { Auth, User } from '../model/public_types';
import { getDefaultEmulatorHost, getExperimentalSetting } from '@firebase/util';

const DEFAULT_ID_TOKEN_MAX_AGE = 5 * 60;
const authIdTokenMaxAge =
getExperimentalSetting('authIdTokenMaxAge') || DEFAULT_ID_TOKEN_MAX_AGE;

let lastPostedIdToken: string | undefined | null = null;

const mintCookieFactory = (url: string) => async (user: User | null) => {
const idTokenResult = user && (await user.getIdTokenResult());
const idTokenAge =
idTokenResult &&
(new Date().getTime() - Date.parse(idTokenResult.issuedAtTime)) / 1_000;
if (idTokenAge && idTokenAge > authIdTokenMaxAge) {
return;
}
// Specifically trip null => undefined when logged out, to delete any existing cookie
const idToken = idTokenResult?.token;
if (lastPostedIdToken === idToken) {
return;
}
lastPostedIdToken = idToken;
await fetch(url, {
method: idToken ? 'POST' : 'DELETE',
headers: idToken
? {
'Authorization': `Bearer ${idToken}`
}
: {}
});
};

/**
* Returns the Auth instance associated with the provided {@link @firebase/app#FirebaseApp}.
Expand All @@ -41,14 +77,30 @@ export function getAuth(app: FirebaseApp = getApp()): Auth {
return provider.getImmediate();
}

return initializeAuth(app, {
const auth = initializeAuth(app, {
popupRedirectResolver: browserPopupRedirectResolver,
persistence: [
indexedDBLocalPersistence,
browserLocalPersistence,
browserSessionPersistence
]
});

const authTokenSyncUrl = getExperimentalSetting('authTokenSyncURL');
if (authTokenSyncUrl) {
const mintCookie = mintCookieFactory(authTokenSyncUrl);
beforeAuthStateChanged(auth, mintCookie, () =>
mintCookie(auth.currentUser)
);
onIdTokenChanged(auth, user => mintCookie(user));
}

const authEmulatorHost = getDefaultEmulatorHost('auth');
if (authEmulatorHost) {
connectAuthEmulator(auth, `http://${authEmulatorHost}`);
}

return auth;
}

registerAuth(ClientPlatform.BROWSER);
12 changes: 10 additions & 2 deletions packages/auth/src/platform_node/index.ts
Expand Up @@ -21,13 +21,14 @@ import { _createError } from '../core/util/assert';
import { FirebaseApp, getApp, _getProvider } from '@firebase/app';
import { Auth } from '../model/public_types';

import { initializeAuth, inMemoryPersistence } from '..';
import { initializeAuth, inMemoryPersistence, connectAuthEmulator } from '..';
import { registerAuth } from '../core/auth/register';
import { ClientPlatform } from '../core/util/version';
import { AuthImpl } from '../core/auth/auth_impl';

import { FetchProvider } from '../core/util/fetch_provider';
import * as fetchImpl from 'node-fetch';
import { getDefaultEmulatorHost } from '@firebase/util';

// Initialize the fetch polyfill, the types are slightly off so just cast and hope for the best
FetchProvider.initialize(
Expand All @@ -46,7 +47,14 @@ export function getAuth(app: FirebaseApp = getApp()): Auth {
return provider.getImmediate();
}

return initializeAuth(app);
const auth = initializeAuth(app);

const authEmulatorHost = getDefaultEmulatorHost('auth');
if (authEmulatorHost) {
connectAuthEmulator(auth, `http://${authEmulatorHost}`);
}

return auth;
}

registerAuth(ClientPlatform.NODE);
Expand Down
11 changes: 9 additions & 2 deletions packages/database/src/api/Database.ts
Expand Up @@ -27,7 +27,8 @@ import { Provider } from '@firebase/component';
import {
getModularInstance,
createMockUserToken,
EmulatorMockTokenOptions
EmulatorMockTokenOptions,
getDefaultEmulatorHost
} from '@firebase/util';

import { AppCheckTokenProvider } from '../core/AppCheckTokenProvider';
Expand Down Expand Up @@ -316,9 +317,15 @@ export function getDatabase(
app: FirebaseApp = getApp(),
url?: string
): Database {
return _getProvider(app, 'database').getImmediate({
const db = _getProvider(app, 'database').getImmediate({
identifier: url
}) as Database;
const databaseEmulatorHost = getDefaultEmulatorHost('database');
if (databaseEmulatorHost) {
const [host, port] = databaseEmulatorHost.split(':');
connectDatabaseEmulator(db, host, parseInt(port, 10));
}
return db;
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/firestore/externs.json
Expand Up @@ -26,6 +26,7 @@
"packages/logger/dist/src/logger.d.ts",
"packages/webchannel-wrapper/src/index.d.ts",
"packages/util/dist/src/crypt.d.ts",
"packages/util/dist/src/defaults.d.ts",
"packages/util/dist/src/emulator.d.ts",
"packages/util/dist/src/environment.d.ts",
"packages/util/dist/src/compat.d.ts",
Expand Down
36 changes: 23 additions & 13 deletions packages/firestore/src/api/database.ts
Expand Up @@ -15,14 +15,13 @@
* limitations under the License.
*/

// eslint-disable-next-line import/no-extraneous-dependencies
import {
_getProvider,
_removeServiceInstance,
FirebaseApp,
getApp
} from '@firebase/app';
import { deepEqual } from '@firebase/util';
import { deepEqual, getDefaultEmulatorHost } from '@firebase/util';

import { User } from '../auth/user';
import {
Expand All @@ -43,7 +42,10 @@ import {
setOnlineComponentProvider
} from '../core/firestore_client';
import { makeDatabaseInfo } from '../lite-api/components';
import { Firestore as LiteFirestore } from '../lite-api/database';
import {
Firestore as LiteFirestore,
connectFirestoreEmulator
} from '../lite-api/database';
import { Query } from '../lite-api/reference';
import {
indexedDbClearPersistence,
Expand Down Expand Up @@ -186,14 +188,6 @@ export function initializeFirestore(
});
}

/**
* Returns the existing default {@link Firestore} instance that is associated with the
* default {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new
* instance with default settings.
*
* @returns The {@link Firestore} instance of the provided app.
*/
export function getFirestore(): Firestore;
/**
* Returns the existing default {@link Firestore} instance that is associated with the
* provided {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new
Expand All @@ -215,7 +209,15 @@ export function getFirestore(app: FirebaseApp): Firestore;
*/
export function getFirestore(databaseId: string): Firestore;
/**
* Returns the existing {@link Firestore} instance that is associated with the
* Returns the existing default {@link Firestore} instance that is associated with the
* default {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new
* instance with default settings.
*
* @returns The {@link Firestore} instance of the provided app.
*/
export function getFirestore(): Firestore;
/**
* Returns the existing default {@link Firestore} instance that is associated with the
* provided {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new
* instance with default settings.
*
Expand All @@ -236,9 +238,17 @@ export function getFirestore(
typeof appOrDatabaseId === 'string'
? appOrDatabaseId
: optionalDatabaseId || DEFAULT_DATABASE_NAME;
return _getProvider(app, 'firestore').getImmediate({
const db = _getProvider(app, 'firestore').getImmediate({
identifier: databaseId
}) as Firestore;
if (!db._initialized) {
const firestoreEmulatorHost = getDefaultEmulatorHost('firestore');
if (firestoreEmulatorHost) {
const [host, port] = firestoreEmulatorHost.split(':');
connectFirestoreEmulator(db, host, parseInt(port, 10));
}
}
return db;
}

/**
Expand Down

0 comments on commit fdd4ab4

Please sign in to comment.