diff --git a/.changeset/heavy-spiders-fry.md b/.changeset/heavy-spiders-fry.md new file mode 100644 index 00000000000..f10664373c2 --- /dev/null +++ b/.changeset/heavy-spiders-fry.md @@ -0,0 +1,6 @@ +--- +'@firebase/firestore': minor +'@firebase/firestore-compat': patch +--- + +Add named DB support diff --git a/common/api-review/firestore-lite.api.md b/common/api-review/firestore-lite.api.md index a96f3ada5cb..c220ad0810f 100644 --- a/common/api-review/firestore-lite.api.md +++ b/common/api-review/firestore-lite.api.md @@ -176,13 +176,22 @@ export function getDoc(reference: DocumentReference): Promise(query: Query): Promise>; // @public -export function getFirestore(app?: FirebaseApp): Firestore; +export function getFirestore(): Firestore; + +// @public +export function getFirestore(app: FirebaseApp): Firestore; + +// @public +export function getFirestore(databaseId: string): Firestore; + +// @public +export function getFirestore(app: FirebaseApp, databaseId: string): Firestore; // @public export function increment(n: number): FieldValue; // @public -export function initializeFirestore(app: FirebaseApp, settings: Settings): Firestore; +export function initializeFirestore(app: FirebaseApp, settings: Settings, databaseId?: string): Firestore; // @public export function limit(limit: number): QueryConstraint; diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index db36b60482c..4e6e29b883b 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -228,13 +228,22 @@ export function getDocsFromCache(query: Query): Promise>; export function getDocsFromServer(query: Query): Promise>; // @public -export function getFirestore(app?: FirebaseApp): Firestore; +export function getFirestore(): Firestore; + +// @public +export function getFirestore(app: FirebaseApp): Firestore; + +// @public +export function getFirestore(databaseId: string): Firestore; + +// @public +export function getFirestore(app: FirebaseApp, databaseId: string): Firestore; // @public export function increment(n: number): FieldValue; // @public -export function initializeFirestore(app: FirebaseApp, settings: FirestoreSettings): Firestore; +export function initializeFirestore(app: FirebaseApp, settings: FirestoreSettings, databaseId?: string): Firestore; // @public export function limit(limit: number): QueryConstraint; diff --git a/integration/firestore/firebase_export.ts b/integration/firestore/firebase_export.ts index 31371b34552..4646e11fe2c 100644 --- a/integration/firestore/firebase_export.ts +++ b/integration/firestore/firebase_export.ts @@ -27,19 +27,25 @@ import { let appCount = 0; +export function newTestApp(projectId: string, appName?: string): FirebaseApp { + if (appName === undefined) { + appName = 'test-app-' + appCount++; + } + return initializeApp( + { + apiKey: 'fake-api-key', + projectId + }, + appName + ); +} + export function newTestFirestore( - projectId: string, - nameOrApp?: string | FirebaseApp, - settings?: FirestoreSettings + app: FirebaseApp, + settings?: FirestoreSettings, + dbName?: string ): Firestore { - if (nameOrApp === undefined) { - nameOrApp = 'test-app-' + appCount++; - } - const app = - typeof nameOrApp === 'string' - ? initializeApp({ apiKey: 'fake-api-key', projectId }, nameOrApp) - : nameOrApp; - return initializeFirestore(app, settings || {}); + return initializeFirestore(app, settings || {}, dbName); } export * from '@firebase/firestore'; diff --git a/packages/firestore-compat/src/index.console.ts b/packages/firestore-compat/src/index.console.ts index 7c9a7fc63a2..9adfdefdfd2 100644 --- a/packages/firestore-compat/src/index.console.ts +++ b/packages/firestore-compat/src/index.console.ts @@ -91,9 +91,9 @@ export class Firestore extends FirestoreCompat { super( databaseIdFromFirestoreDatabase(firestoreDatabase), new FirestoreExp( - databaseIdFromFirestoreDatabase(firestoreDatabase), new _EmptyAuthCredentialsProvider(), - new _EmptyAppCheckTokenProvider() + new _EmptyAppCheckTokenProvider(), + databaseIdFromFirestoreDatabase(firestoreDatabase) ), new MemoryPersistenceProvider() ); diff --git a/packages/firestore/lite/register.ts b/packages/firestore/lite/register.ts index 8409aad6aaf..9bd7b014fa2 100644 --- a/packages/firestore/lite/register.ts +++ b/packages/firestore/lite/register.ts @@ -27,9 +27,9 @@ import { LiteAppCheckTokenProvider, LiteAuthCredentialsProvider } from '../src/api/credentials'; +import { databaseIdFromApp } from '../src/core/database_info'; import { setSDKVersion } from '../src/core/version'; import { Firestore } from '../src/lite-api/database'; -import { FirestoreSettings } from '../src/lite-api/settings'; declare module '@firebase/component' { interface NameServiceMapping { @@ -42,16 +42,17 @@ export function registerFirestore(): void { _registerComponent( new Component( 'firestore/lite', - (container, { options: settings }: { options?: FirestoreSettings }) => { + (container, { instanceIdentifier: databaseId, options: settings }) => { const app = container.getProvider('app').getImmediate()!; const firestoreInstance = new Firestore( - app, new LiteAuthCredentialsProvider( container.getProvider('auth-internal') ), new LiteAppCheckTokenProvider( container.getProvider('app-check-internal') - ) + ), + databaseIdFromApp(app, databaseId), + app ); if (settings) { firestoreInstance._setSettings(settings); @@ -59,7 +60,7 @@ export function registerFirestore(): void { return firestoreInstance; }, 'PUBLIC' as ComponentType.PUBLIC - ) + ).setMultipleInstances(true) ); // RUNTIME_ENV and BUILD_TARGET are replaced by real values during the compilation registerVersion('firestore-lite', version, '__RUNTIME_ENV__'); diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index 7a71071b54b..5194cfcd462 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -31,7 +31,7 @@ import { OfflineComponentProvider, OnlineComponentProvider } from '../core/component_provider'; -import { DatabaseId } from '../core/database_info'; +import { DatabaseId, DEFAULT_DATABASE_NAME } from '../core/database_info'; import { FirestoreClient, firestoreClientDisableNetwork, @@ -103,17 +103,18 @@ export class Firestore extends LiteFirestore { /** @hideconstructor */ constructor( - databaseIdOrApp: DatabaseId | FirebaseApp, authCredentialsProvider: CredentialsProvider, - appCheckCredentialsProvider: CredentialsProvider + appCheckCredentialsProvider: CredentialsProvider, + databaseId: DatabaseId, + app?: FirebaseApp ) { super( - databaseIdOrApp, authCredentialsProvider, - appCheckCredentialsProvider + appCheckCredentialsProvider, + databaseId, + app ); - this._persistenceKey = - 'name' in databaseIdOrApp ? databaseIdOrApp.name : '[DEFAULT]'; + this._persistenceKey = app?.name || '[DEFAULT]'; } _terminate(): Promise { @@ -135,17 +136,26 @@ export class Firestore extends LiteFirestore { * @param app - The {@link @firebase/app#FirebaseApp} with which the {@link Firestore} instance will * be associated. * @param settings - A settings object to configure the {@link Firestore} instance. + * @param databaseId - The name of database. * @returns A newly initialized {@link Firestore} instance. */ export function initializeFirestore( app: FirebaseApp, - settings: FirestoreSettings + settings: FirestoreSettings, + databaseId?: string ): Firestore { + if (!databaseId) { + databaseId = DEFAULT_DATABASE_NAME; + } const provider = _getProvider(app, 'firestore'); - if (provider.isInitialized()) { - const existingInstance = provider.getImmediate(); - const initialSettings = provider.getOptions() as FirestoreSettings; + if (provider.isInitialized(databaseId)) { + const existingInstance = provider.getImmediate({ + identifier: databaseId + }); + const initialSettings = provider.getOptions( + databaseId + ) as FirestoreSettings; if (deepEqual(initialSettings, settings)) { return existingInstance; } else { @@ -170,9 +180,39 @@ export function initializeFirestore( ); } - return provider.initialize({ options: settings }); + return provider.initialize({ + options: settings, + instanceIdentifier: databaseId + }); } +/** + * 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. + * + * @param app - The {@link @firebase/app#FirebaseApp} instance that the returned {@link Firestore} + * instance is associated with. + * @returns The {@link Firestore} instance of the provided app. + */ +export function getFirestore(app: FirebaseApp): Firestore; +/** + * Returns the existing {@link Firestore} instance that is associated with the + * default {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new + * instance with default settings. + * + * @param databaseId - The name of database. + * @returns The {@link Firestore} instance of the provided app. + */ +export function getFirestore(databaseId: string): Firestore; /** * Returns the existing {@link Firestore} instance that is associated with the * provided {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new @@ -180,10 +220,23 @@ export function initializeFirestore( * * @param app - The {@link @firebase/app#FirebaseApp} instance that the returned {@link Firestore} * instance is associated with. + * @param databaseId - The name of database. * @returns The {@link Firestore} instance of the provided app. */ -export function getFirestore(app: FirebaseApp = getApp()): Firestore { - return _getProvider(app, 'firestore').getImmediate() as Firestore; +export function getFirestore(app: FirebaseApp, databaseId: string): Firestore; +export function getFirestore( + appOrDatabaseId?: FirebaseApp | string, + optionalDatabaseId?: string +): Firestore { + const app: FirebaseApp = + typeof appOrDatabaseId === 'object' ? appOrDatabaseId : getApp(); + const databaseId = + typeof appOrDatabaseId === 'string' + ? appOrDatabaseId + : optionalDatabaseId || DEFAULT_DATABASE_NAME; + return _getProvider(app, 'firestore').getImmediate({ + identifier: databaseId + }) as Firestore; } /** @@ -498,7 +551,11 @@ export function disableNetwork(firestore: Firestore): Promise { * terminated. */ export function terminate(firestore: Firestore): Promise { - _removeServiceInstance(firestore.app, 'firestore'); + _removeServiceInstance( + firestore.app, + 'firestore', + firestore._databaseId.database + ); return firestore._delete(); } diff --git a/packages/firestore/src/core/database_info.ts b/packages/firestore/src/core/database_info.ts index c03afb84ac6..306a9920ea9 100644 --- a/packages/firestore/src/core/database_info.ts +++ b/packages/firestore/src/core/database_info.ts @@ -1,3 +1,7 @@ +import { FirebaseApp } from '@firebase/app'; + +import { Code, FirestoreError } from '../util/error'; + /** * @license * Copyright 2017 Google LLC @@ -46,7 +50,7 @@ export class DatabaseInfo { } /** The default database name for a project. */ -const DEFAULT_DATABASE_NAME = '(default)'; +export const DEFAULT_DATABASE_NAME = '(default)'; /** * Represents the database ID a Firestore client is associated with. @@ -74,3 +78,17 @@ export class DatabaseId { ); } } + +export function databaseIdFromApp( + app: FirebaseApp, + database?: string +): DatabaseId { + if (!Object.prototype.hasOwnProperty.apply(app.options, ['projectId'])) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + '"projectId" not provided in firebase.initializeApp.' + ); + } + + return new DatabaseId(app.options.projectId!, database); +} diff --git a/packages/firestore/src/lite-api/database.ts b/packages/firestore/src/lite-api/database.ts index 07db0a3c868..8cfd6577943 100644 --- a/packages/firestore/src/lite-api/database.ts +++ b/packages/firestore/src/lite-api/database.ts @@ -31,7 +31,7 @@ import { OAuthToken } from '../api/credentials'; import { User } from '../auth/user'; -import { DatabaseId } from '../core/database_info'; +import { DatabaseId, DEFAULT_DATABASE_NAME } from '../core/database_info'; import { Code, FirestoreError } from '../util/error'; import { cast } from '../util/input_validation'; import { logWarn } from '../util/log'; @@ -61,7 +61,6 @@ export class Firestore implements FirestoreService { */ type: 'firestore-lite' | 'firestore' = 'firestore-lite'; - readonly _databaseId: DatabaseId; readonly _persistenceKey: string = '(lite)'; private _settings = new FirestoreSettingsImpl({}); @@ -71,21 +70,13 @@ export class Firestore implements FirestoreService { // all components have shut down. private _terminateTask?: Promise; - _app?: FirebaseApp; - /** @hideconstructor */ constructor( - databaseIdOrApp: DatabaseId | FirebaseApp, public _authCredentials: CredentialsProvider, - public _appCheckCredentials: CredentialsProvider - ) { - if (databaseIdOrApp instanceof DatabaseId) { - this._databaseId = databaseIdOrApp; - } else { - this._app = databaseIdOrApp as FirebaseApp; - this._databaseId = databaseIdFromApp(databaseIdOrApp as FirebaseApp); - } - } + public _appCheckCredentials: CredentialsProvider, + readonly _databaseId: DatabaseId, + readonly _app?: FirebaseApp + ) {} /** * The {@link @firebase/app#FirebaseApp} associated with this `Firestore` service @@ -163,17 +154,6 @@ export class Firestore implements FirestoreService { } } -function databaseIdFromApp(app: FirebaseApp): DatabaseId { - if (!Object.prototype.hasOwnProperty.apply(app.options, ['projectId'])) { - throw new FirestoreError( - Code.INVALID_ARGUMENT, - '"projectId" not provided in firebase.initializeApp.' - ); - } - - return new DatabaseId(app.options.projectId!); -} - /** * Initializes a new instance of Cloud Firestore with the provided settings. * Can only be called before any other functions, including @@ -183,35 +163,83 @@ function databaseIdFromApp(app: FirebaseApp): DatabaseId { * @param app - The {@link @firebase/app#FirebaseApp} with which the `Firestore` instance will * be associated. * @param settings - A settings object to configure the `Firestore` instance. + * @param databaseId - The name of database. * @returns A newly initialized `Firestore` instance. */ export function initializeFirestore( app: FirebaseApp, - settings: FirestoreSettings + settings: FirestoreSettings, + databaseId?: string ): Firestore { + if (!databaseId) { + databaseId = DEFAULT_DATABASE_NAME; + } const provider = _getProvider(app, 'firestore/lite'); - if (provider.isInitialized()) { + if (provider.isInitialized(databaseId)) { throw new FirestoreError( Code.FAILED_PRECONDITION, 'Firestore can only be initialized once per app.' ); } - return provider.initialize({ options: settings }); + return provider.initialize({ + options: settings, + instanceIdentifier: databaseId + }); } /** - * Returns the existing `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. * - * @param app - The {@link @firebase/app#FirebaseApp} instance that the returned `Firestore` + * @param app - The {@link @firebase/app#FirebaseApp} instance that the returned {@link Firestore} * instance is associated with. - * @returns The `Firestore` instance of the provided app. + * @returns The {@link Firestore} instance of the provided app. */ -export function getFirestore(app: FirebaseApp = getApp()): Firestore { - return _getProvider(app, 'firestore/lite').getImmediate() as Firestore; +export function getFirestore(app: FirebaseApp): Firestore; +/** + * Returns the existing {@link Firestore} instance that is associated with the + * default {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new + * instance with default settings. + * + * @param databaseId - The name of database. + * @returns The {@link Firestore} instance of the provided app. + */ +export function getFirestore(databaseId: string): Firestore; +/** + * Returns the existing {@link Firestore} instance that is associated with the + * provided {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new + * instance with default settings. + * + * @param app - The {@link @firebase/app#FirebaseApp} instance that the returned {@link Firestore} + * instance is associated with. + * @param databaseId - The name of database. + * @returns The {@link Firestore} instance of the provided app. + */ +export function getFirestore(app: FirebaseApp, databaseId: string): Firestore; +export function getFirestore( + appOrDatabaseId?: FirebaseApp | string, + optionalDatabaseId?: string +): Firestore { + const app: FirebaseApp = + typeof appOrDatabaseId === 'object' ? appOrDatabaseId : getApp(); + const databaseId = + typeof appOrDatabaseId === 'string' + ? appOrDatabaseId + : optionalDatabaseId || '(default)'; + return _getProvider(app, 'firestore/lite').getImmediate({ + identifier: databaseId + }) as Firestore; } export { EmulatorMockTokenOptions } from '@firebase/util'; diff --git a/packages/firestore/src/register.ts b/packages/firestore/src/register.ts index cf4cb83632a..3abb38c9d86 100644 --- a/packages/firestore/src/register.ts +++ b/packages/firestore/src/register.ts @@ -30,7 +30,7 @@ import { import { setSDKVersion } from '../src/core/version'; import { Firestore } from './api/database'; -import { PrivateSettings } from './lite-api/settings'; +import { databaseIdFromApp } from './core/database_info'; export function registerFirestore( variant?: string, @@ -40,23 +40,24 @@ export function registerFirestore( _registerComponent( new Component( 'firestore', - (container, { options: settings }: { options?: PrivateSettings }) => { + (container, { instanceIdentifier: databaseId, options: settings }) => { const app = container.getProvider('app').getImmediate()!; const firestoreInstance = new Firestore( - app, new FirebaseAuthCredentialsProvider( container.getProvider('auth-internal') ), new FirebaseAppCheckTokenProvider( container.getProvider('app-check-internal') - ) + ), + databaseIdFromApp(app, databaseId), + app ); settings = { useFetchStreams, ...settings }; firestoreInstance._setSettings(settings); return firestoreInstance; }, 'PUBLIC' as ComponentType.PUBLIC - ) + ).setMultipleInstances(true) ); registerVersion(name, version, variant); // BUILD_TARGET will be replaced by values like esm5, esm2017, cjs5, etc during the compilation diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index 5609d86a8b4..5f3e2dc6c61 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -62,7 +62,8 @@ import { Timestamp, FieldPath, newTestFirestore, - SnapshotOptions + SnapshotOptions, + newTestApp } from '../util/firebase_export'; import { apiDescribe, @@ -71,7 +72,8 @@ import { withTestDb, withTestDbs, withTestDoc, - withTestDocAndInitialData + withTestDocAndInitialData, + withNamedTestDbsOrSkipUnlessUsingEmulator } from '../util/helpers'; import { DEFAULT_SETTINGS, DEFAULT_PROJECT_ID } from '../util/settings'; @@ -1106,8 +1108,7 @@ apiDescribe('Database', (persistence: boolean) => { await deleteApp(app); const firestore2 = newTestFirestore( - options.projectId!, - name, + newTestApp(options.projectId!, name), DEFAULT_SETTINGS ); await enableIndexedDbPersistence(firestore2); @@ -1149,7 +1150,9 @@ apiDescribe('Database', (persistence: boolean) => { await deleteApp(app); - const firestore2 = newTestFirestore(options.projectId!, name); + const firestore2 = newTestFirestore( + newTestApp(options.projectId!, name) + ); await enableIndexedDbPersistence(firestore2); const docRef2 = doc(firestore2, docRef.path); const docSnap2 = await getDocFromCache(docRef2); @@ -1170,7 +1173,9 @@ apiDescribe('Database', (persistence: boolean) => { await deleteApp(app); await clearIndexedDbPersistence(firestore); - const firestore2 = newTestFirestore(options.projectId!, name); + const firestore2 = newTestFirestore( + newTestApp(options.projectId!, name) + ); await enableIndexedDbPersistence(firestore2); const docRef2 = doc(firestore2, docRef.path); await expect(getDocFromCache(docRef2)).to.eventually.be.rejectedWith( @@ -1191,7 +1196,9 @@ apiDescribe('Database', (persistence: boolean) => { const options = app.options; await deleteApp(app); - const firestore2 = newTestFirestore(options.projectId!, name); + const firestore2 = newTestFirestore( + newTestApp(options.projectId!, name) + ); await clearIndexedDbPersistence(firestore2); await enableIndexedDbPersistence(firestore2); const docRef2 = doc(firestore2, docRef.path); @@ -1701,4 +1708,47 @@ apiDescribe('Database', (persistence: boolean) => { } ); }); + + it('can keep docs separate with multi-db when online', () => { + return withNamedTestDbsOrSkipUnlessUsingEmulator( + persistence, + ['db1', 'db2'], + async ([db1, db2]) => { + const data = { name: 'Rafi', email: 'abc@xyz.com' }; + + const ref1 = await doc(collection(db1, 'users'), 'doc1'); + await setDoc(ref1, data); + const snapshot1 = await getDoc(ref1); + expect(snapshot1.exists()).to.be.ok; + expect(snapshot1.data()).to.be.deep.equals(data); + + const ref2 = await doc(collection(db2, 'users'), 'doc1'); + const snapshot2 = await getDocFromServer(ref2); + expect(snapshot2.exists()).to.not.be.ok; + } + ); + }); + + it('can keep docs separate with multi-db when offline', () => { + return withNamedTestDbsOrSkipUnlessUsingEmulator( + persistence, + ['db1', 'db2'], + async ([db1, db2]) => { + await disableNetwork(db1); + await disableNetwork(db2); + const data = { name: 'Rafi', email: 'abc@xyz.com' }; + + const ref1 = await doc(collection(db1, 'users')); + void setDoc(ref1, data); + const snapshot = await getDocFromCache(ref1); + expect(snapshot.exists()).to.be.ok; + expect(snapshot.data()).to.be.deep.equals(data); + + const ref2 = await doc(collection(db2, 'users')); + await expect(getDocFromCache(ref2)).to.eventually.rejectedWith( + 'Failed to get document from cache.' + ); + } + ); + }); }); diff --git a/packages/firestore/test/integration/api/provider.test.ts b/packages/firestore/test/integration/api/provider.test.ts new file mode 100644 index 00000000000..ffcd0e7f350 --- /dev/null +++ b/packages/firestore/test/integration/api/provider.test.ts @@ -0,0 +1,161 @@ +/** + * @license + * Copyright 2022 Google LLC + * + * 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 { initializeApp } from '@firebase/app'; +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; + +import { + doc, + getFirestore, + initializeFirestore, + Firestore, + terminate, + getDoc +} from '../util/firebase_export'; +import { DEFAULT_SETTINGS } from '../util/settings'; + +use(chaiAsPromised); + +describe('Firestore Provider', () => { + it('can provide setting', () => { + const app = initializeApp( + { apiKey: 'fake-api-key', projectId: 'test-project' }, + 'test-app-initializeFirestore' + ); + const fs1 = initializeFirestore(app, { host: 'localhost', ssl: false }); + expect(fs1).to.be.an.instanceOf(Firestore); + }); + + it('returns same default instance from named app', () => { + const app = initializeApp( + { apiKey: 'fake-api-key', projectId: 'test-project' }, + 'test-app-getFirestore' + ); + const fs1 = getFirestore(app); + const fs2 = getFirestore(app); + const fs3 = getFirestore(app, '(default)'); + expect(fs1).to.be.equal(fs2).and.equal(fs3); + }); + + it('returns different instance from named app', () => { + const app = initializeApp( + { apiKey: 'fake-api-key', projectId: 'test-project' }, + 'test-app-getFirestore' + ); + const fs1 = initializeFirestore(app, DEFAULT_SETTINGS, 'init1'); + const fs2 = initializeFirestore(app, DEFAULT_SETTINGS, 'init2'); + const fs3 = getFirestore(app); + const fs4 = getFirestore(app, 'name1'); + const fs5 = getFirestore(app, 'name2'); + expect(fs1).to.not.be.equal(fs2); + expect(fs1).to.not.be.equal(fs3); + expect(fs1).to.not.be.equal(fs4); + expect(fs1).to.not.be.equal(fs5); + expect(fs2).to.not.be.equal(fs3); + expect(fs2).to.not.be.equal(fs4); + expect(fs2).to.not.be.equal(fs5); + expect(fs3).to.not.be.equal(fs4); + expect(fs3).to.not.be.equal(fs5); + expect(fs4).to.not.be.equal(fs5); + }); + + it('returns same default instance from default app', () => { + const app = initializeApp({ + apiKey: 'fake-api-key', + projectId: 'test-project' + }); + const fs1 = initializeFirestore(app, DEFAULT_SETTINGS); + const fs2 = initializeFirestore(app, DEFAULT_SETTINGS); + const fs3 = getFirestore(); + const fs4 = getFirestore(app); + const fs5 = getFirestore('(default)'); + const fs6 = getFirestore(app, '(default)'); + expect(fs1).to.be.equal(fs2); + expect(fs1).to.be.equal(fs3); + expect(fs1).to.be.equal(fs4); + expect(fs1).to.be.equal(fs5); + expect(fs1).to.be.equal(fs6); + }); + + it('returns different instance from different named app', () => { + initializeApp({ apiKey: 'fake-api-key', projectId: 'test-project' }); + const app1 = initializeApp( + { apiKey: 'fake-api-key', projectId: 'test-project' }, + 'test-app-getFirestore-1' + ); + const app2 = initializeApp( + { apiKey: 'fake-api-key', projectId: 'test-project' }, + 'test-app-getFirestore-2' + ); + const fs1 = getFirestore(); + const fs2 = getFirestore(app1); + const fs3 = getFirestore(app2); + expect(fs1).to.not.be.equal(fs2); + expect(fs1).to.not.be.equal(fs3); + expect(fs2).to.not.be.equal(fs3); + }); + + it('can call initializeFirestore() twice if settings are same', () => { + const app = initializeApp( + { apiKey: 'fake-api-key', projectId: 'test-project' }, + 'test-app-initializeFirestore-twice' + ); + const fs1 = initializeFirestore(app, DEFAULT_SETTINGS); + const fs2 = initializeFirestore(app, DEFAULT_SETTINGS); + expect(fs1).to.be.equal(fs2); + }); + + it('cannot use once terminated', () => { + const app = initializeApp( + { apiKey: 'fake-api-key', projectId: 'test-project' }, + 'test-app-terminated' + ); + const firestore = initializeFirestore(app, { + host: 'localhost', + ssl: false + }); + + // We don't await the Promise. Any operation enqueued after should be + // rejected. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + terminate(firestore); + + try { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + getDoc(doc(firestore, 'coll/doc')); + expect.fail(); + } catch (e) { + expect((e as Error)?.message).to.equal( + 'The client has already been terminated.' + ); + } + }); + + it('can call terminate() multiple times', () => { + const app = initializeApp( + { apiKey: 'fake-api-key', projectId: 'test-project' }, + 'test-app-multi-terminate' + ); + const firestore = initializeFirestore(app, { + host: 'localhost', + ssl: false + }); + + return terminate(firestore).then(() => terminate(firestore)); + }); +}); diff --git a/packages/firestore/test/integration/api/validation.test.ts b/packages/firestore/test/integration/api/validation.test.ts index a61b5595ec0..2943a8b0c06 100644 --- a/packages/firestore/test/integration/api/validation.test.ts +++ b/packages/firestore/test/integration/api/validation.test.ts @@ -49,7 +49,8 @@ import { serverTimestamp, setDoc, updateDoc, - where + where, + newTestApp } from '../util/firebase_export'; import { apiDescribe, @@ -156,19 +157,19 @@ apiDescribe('Validation:', (persistence: boolean) => { validationIt(persistence, 'enforces minimum cache size', () => { expect(() => - newTestFirestore('test-project', undefined, { cacheSizeBytes: 1 }) + newTestFirestore(newTestApp('test-project'), { cacheSizeBytes: 1 }) ).to.throw('cacheSizeBytes must be at least 1048576'); }); validationIt(persistence, 'garbage collection can be disabled', () => { // Verify that this doesn't throw. - newTestFirestore('test-project', undefined, { + newTestFirestore(newTestApp('test-project'), { cacheSizeBytes: /* CACHE_SIZE_UNLIMITED= */ -1 }); }); validationIt(persistence, 'useEmulator can set host and port', () => { - const db = newTestFirestore('test-project'); + const db = newTestFirestore(newTestApp('test-project')); // Verify that this doesn't throw. connectFirestoreEmulator(db, 'localhost', 9000); }); @@ -191,7 +192,7 @@ apiDescribe('Validation:', (persistence: boolean) => { persistence, 'useEmulator can set mockUserToken object', () => { - const db = newTestFirestore('test-project'); + const db = newTestFirestore(newTestApp('test-project')); // Verify that this doesn't throw. connectFirestoreEmulator(db, 'localhost', 9000, { mockUserToken: { sub: 'foo' } @@ -203,7 +204,7 @@ apiDescribe('Validation:', (persistence: boolean) => { persistence, 'useEmulator can set mockUserToken string', () => { - const db = newTestFirestore('test-project'); + const db = newTestFirestore(newTestApp('test-project')); // Verify that this doesn't throw. connectFirestoreEmulator(db, 'localhost', 9000, { mockUserToken: 'my-mock-user-token' diff --git a/packages/firestore/test/integration/util/firebase_export.ts b/packages/firestore/test/integration/util/firebase_export.ts index ab52cb2e1db..f58b3ce045b 100644 --- a/packages/firestore/test/integration/util/firebase_export.ts +++ b/packages/firestore/test/integration/util/firebase_export.ts @@ -29,27 +29,25 @@ import { PrivateSettings } from '../../../src/lite-api/settings'; // every test and never clean them up. We may need to revisit. let appCount = 0; -export function newTestFirestore( - projectId: string, - nameOrApp?: string | FirebaseApp, - settings?: PrivateSettings -): Firestore { - if (nameOrApp === undefined) { - nameOrApp = 'test-app-' + appCount++; +export function newTestApp(projectId: string, appName?: string): FirebaseApp { + if (appName === undefined) { + appName = 'test-app-' + appCount++; } + return initializeApp( + { + apiKey: 'fake-api-key', + projectId + }, + appName + ); +} - const app = - typeof nameOrApp === 'string' - ? initializeApp( - { - apiKey: 'fake-api-key', - projectId - }, - nameOrApp - ) - : nameOrApp; - - return initializeFirestore(app, settings || {}); +export function newTestFirestore( + app: FirebaseApp, + settings?: PrivateSettings, + dbName?: string +): Firestore { + return initializeFirestore(app, settings || {}, dbName); } export * from '../../../src'; diff --git a/packages/firestore/test/integration/util/helpers.ts b/packages/firestore/test/integration/util/helpers.ts index 0a8dd3a5108..f2584916daf 100644 --- a/packages/firestore/test/integration/util/helpers.ts +++ b/packages/firestore/test/integration/util/helpers.ts @@ -31,12 +31,14 @@ import { setDoc, PrivateSettings, SnapshotListenOptions, - newTestFirestore + newTestFirestore, + newTestApp } from './firebase_export'; import { ALT_PROJECT_ID, DEFAULT_PROJECT_ID, - DEFAULT_SETTINGS + DEFAULT_SETTINGS, + USE_EMULATOR } from './settings'; /* eslint-disable no-restricted-globals */ @@ -182,7 +184,41 @@ export async function withTestDbsSettings( const dbs: Firestore[] = []; for (let i = 0; i < numDbs; i++) { - const db = newTestFirestore(projectId, /* name =*/ undefined, settings); + const db = newTestFirestore(newTestApp(projectId), settings); + if (persistence) { + await enableIndexedDbPersistence(db); + } + dbs.push(db); + } + + try { + await fn(dbs); + } finally { + for (const db of dbs) { + await terminate(db); + if (persistence) { + await clearIndexedDbPersistence(db); + } + } + } +} + +export async function withNamedTestDbsOrSkipUnlessUsingEmulator( + persistence: boolean, + dbNames: string[], + fn: (db: Firestore[]) => Promise +): Promise { + // Tests with named DBs can only run on emulator for now. This is because the + // emulator does not require DB to be created before use. + // TODO: Design ability to run named DB tests on backend. Maybe create DBs + // TODO: beforehand, or create DBs as part of test setup. + if (!USE_EMULATOR) { + return Promise.resolve(); + } + const app = newTestApp(DEFAULT_PROJECT_ID); + const dbs: Firestore[] = []; + for (const dbName of dbNames) { + const db = newTestFirestore(app, DEFAULT_SETTINGS, dbName); if (persistence) { await enableIndexedDbPersistence(db); } diff --git a/packages/firestore/test/lite/integration.test.ts b/packages/firestore/test/lite/integration.test.ts index deaa9a0236f..262a0489b05 100644 --- a/packages/firestore/test/lite/integration.test.ts +++ b/packages/firestore/test/lite/integration.test.ts @@ -106,14 +106,71 @@ describe('Firestore', () => { expect(fs1).to.be.an.instanceOf(Firestore); }); - it('returns same instance', () => { + it('returns same default instance from named app', () => { const app = initializeApp( { apiKey: 'fake-api-key', projectId: 'test-project' }, 'test-app-getFirestore' ); const fs1 = getFirestore(app); const fs2 = getFirestore(app); - expect(fs1 === fs2).to.be.true; + const fs3 = getFirestore(app, '(default)'); + expect(fs1).to.be.equal(fs2).and.equal(fs3); + }); + + it('returns different instance from named app', () => { + const app = initializeApp( + { apiKey: 'fake-api-key', projectId: 'test-project' }, + 'test-app-getFirestore' + ); + const fs1 = initializeFirestore(app, DEFAULT_SETTINGS, 'init1'); + const fs2 = initializeFirestore(app, DEFAULT_SETTINGS, 'init2'); + const fs3 = getFirestore(app); + const fs4 = getFirestore(app, 'name1'); + const fs5 = getFirestore(app, 'name2'); + expect(fs1).to.not.be.equal(fs2); + expect(fs1).to.not.be.equal(fs3); + expect(fs1).to.not.be.equal(fs4); + expect(fs1).to.not.be.equal(fs5); + expect(fs2).to.not.be.equal(fs3); + expect(fs2).to.not.be.equal(fs4); + expect(fs2).to.not.be.equal(fs5); + expect(fs3).to.not.be.equal(fs4); + expect(fs3).to.not.be.equal(fs5); + expect(fs4).to.not.be.equal(fs5); + }); + + it('returns same default instance from default app', () => { + const app = initializeApp({ + apiKey: 'fake-api-key', + projectId: 'test-project' + }); + const fs1 = initializeFirestore(app, DEFAULT_SETTINGS); + const fs2 = getFirestore(); + const fs3 = getFirestore(app); + const fs4 = getFirestore('(default)'); + const fs5 = getFirestore(app, '(default)'); + expect(fs1).to.be.equal(fs2); + expect(fs1).to.be.equal(fs3); + expect(fs1).to.be.equal(fs4); + expect(fs1).to.be.equal(fs5); + }); + + it('returns different instance from different named app', () => { + initializeApp({ apiKey: 'fake-api-key', projectId: 'test-project' }); + const app1 = initializeApp( + { apiKey: 'fake-api-key', projectId: 'test-project' }, + 'test-app-getFirestore-1' + ); + const app2 = initializeApp( + { apiKey: 'fake-api-key', projectId: 'test-project' }, + 'test-app-getFirestore-2' + ); + const fs1 = getFirestore(); + const fs2 = getFirestore(app1); + const fs3 = getFirestore(app2); + expect(fs1).to.not.be.equal(fs2); + expect(fs1).to.not.be.equal(fs3); + expect(fs2).to.not.be.equal(fs3); }); it('cannot call initializeFirestore() twice', () => { diff --git a/packages/firestore/test/util/api_helpers.ts b/packages/firestore/test/util/api_helpers.ts index 81d01132087..2dd8a6199e0 100644 --- a/packages/firestore/test/util/api_helpers.ts +++ b/packages/firestore/test/util/api_helpers.ts @@ -58,9 +58,9 @@ export function firestore(): Firestore { export function newTestFirestore(projectId = 'new-project'): Firestore { return new Firestore( - new DatabaseId(projectId), new EmptyAuthCredentialsProvider(), - new EmptyAppCheckTokenProvider() + new EmptyAppCheckTokenProvider(), + new DatabaseId(projectId) ); }