From db8565812159c28a16bb4fc025154a7d68c4eeba Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Tue, 2 Aug 2022 15:08:54 -0400 Subject: [PATCH 01/16] Multi DB Work In Progress --- packages/firestore/lite/register.ts | 4 ++-- packages/firestore/src/api/database.ts | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/firestore/lite/register.ts b/packages/firestore/lite/register.ts index 8409aad6aaf..5d7a8e6efaa 100644 --- a/packages/firestore/lite/register.ts +++ b/packages/firestore/lite/register.ts @@ -42,7 +42,7 @@ export function registerFirestore(): void { _registerComponent( new Component( 'firestore/lite', - (container, { options: settings }: { options?: FirestoreSettings }) => { + (container, { instanceIdentifier: databaseId, options: settings }: { options?: FirestoreSettings }) => { const app = container.getProvider('app').getImmediate()!; const firestoreInstance = new Firestore( app, @@ -59,7 +59,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..9c9ed88ccc2 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -182,8 +182,22 @@ export function initializeFirestore( * instance is associated with. * @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( + 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 || getApp(), 'firestore').getImmediate({ + identifier: databaseId + }) as Firestore; } /** From b13378b46b3ef6600933db0682dacc7789842ce0 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 4 Aug 2022 17:30:59 -0400 Subject: [PATCH 02/16] Work in progress --- packages/firestore/lite/register.ts | 4 ++-- packages/firestore/src/register.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/firestore/lite/register.ts b/packages/firestore/lite/register.ts index 5d7a8e6efaa..2ec5f9a3b0e 100644 --- a/packages/firestore/lite/register.ts +++ b/packages/firestore/lite/register.ts @@ -20,7 +20,7 @@ import { registerVersion, SDK_VERSION } from '@firebase/app'; -import { Component, ComponentType } from '@firebase/component'; +import { Component, ComponentType, InstanceFactoryOptions } from '@firebase/component'; import { version } from '../package.json'; import { @@ -42,7 +42,7 @@ export function registerFirestore(): void { _registerComponent( new Component( 'firestore/lite', - (container, { instanceIdentifier: databaseId, options: settings }: { options?: FirestoreSettings }) => { + (container, { instanceIdentifier: databaseId, options: settings }) => { const app = container.getProvider('app').getImmediate()!; const firestoreInstance = new Firestore( app, diff --git a/packages/firestore/src/register.ts b/packages/firestore/src/register.ts index cf4cb83632a..1bcda165648 100644 --- a/packages/firestore/src/register.ts +++ b/packages/firestore/src/register.ts @@ -40,7 +40,7 @@ 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, From ce6cf1d684124149661190ecd78a77b6723f636c Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 8 Aug 2022 15:49:44 -0400 Subject: [PATCH 03/16] MultiDb with Tests. --- common/api-review/firestore-lite.api.md | 11 ++- common/api-review/firestore.api.md | 11 ++- .../firestore-compat/src/index.console.ts | 4 +- packages/firestore/lite/register.ts | 6 +- packages/firestore/package.json | 1 + packages/firestore/src/api/database.ts | 48 +++++++++--- packages/firestore/src/core/database_info.ts | 14 ++++ packages/firestore/src/lite-api/database.ts | 77 ++++++++++++------- packages/firestore/src/register.ts | 6 +- .../firestore/test/lite/integration.test.ts | 49 +++++++++++- packages/firestore/test/util/api_helpers.ts | 4 +- 11 files changed, 178 insertions(+), 53 deletions(-) diff --git a/common/api-review/firestore-lite.api.md b/common/api-review/firestore-lite.api.md index a96f3ada5cb..c69f9d2da25 100644 --- a/common/api-review/firestore-lite.api.md +++ b/common/api-review/firestore-lite.api.md @@ -176,7 +176,16 @@ 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; diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index db36b60482c..f34e846eb7d 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -228,7 +228,16 @@ 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; 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 2ec5f9a3b0e..2afb9bf0f29 100644 --- a/packages/firestore/lite/register.ts +++ b/packages/firestore/lite/register.ts @@ -30,6 +30,7 @@ import { import { setSDKVersion } from '../src/core/version'; import { Firestore } from '../src/lite-api/database'; import { FirestoreSettings } from '../src/lite-api/settings'; +import {databaseIdFromApp} from "../src/core/database_info"; declare module '@firebase/component' { interface NameServiceMapping { @@ -45,13 +46,14 @@ export function registerFirestore(): void { (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); diff --git a/packages/firestore/package.json b/packages/firestore/package.json index 4f6614d808b..057c6f86f98 100644 --- a/packages/firestore/package.json +++ b/packages/firestore/package.json @@ -86,6 +86,7 @@ "@grpc/grpc-js": "^1.3.2", "@grpc/proto-loader": "^0.6.13", "node-fetch": "2.6.7", + "sqlite3": "^5.0.11", "tslib": "^2.1.0" }, "peerDependencies": { diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index 9c9ed88ccc2..ba87ee17c9b 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -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 { @@ -173,6 +174,14 @@ export function initializeFirestore( return provider.initialize({ options: settings }); } +/** + * 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. + * + * @returns The {@link Firestore} instance of the provided app. + */ +export function getFirestore(): Firestore; /** * Returns the existing {@link Firestore} instance that is associated with the * provided {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new @@ -182,12 +191,29 @@ export function initializeFirestore( * 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 + * 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( - databaseId?: string + databaseId: string ): Firestore; export function getFirestore( appOrDatabaseId?: FirebaseApp | string, @@ -195,7 +221,7 @@ export function getFirestore( ): Firestore { const app: FirebaseApp = typeof appOrDatabaseId === 'object' ? appOrDatabaseId : getApp(); const databaseId = typeof appOrDatabaseId === 'string' ? appOrDatabaseId : optionalDatabaseId || '(default)'; - return _getProvider(app || getApp(), 'firestore').getImmediate({ + return _getProvider(app, 'firestore').getImmediate({ identifier: databaseId }) as Firestore; } diff --git a/packages/firestore/src/core/database_info.ts b/packages/firestore/src/core/database_info.ts index c03afb84ac6..4b0e334c5e6 100644 --- a/packages/firestore/src/core/database_info.ts +++ b/packages/firestore/src/core/database_info.ts @@ -1,3 +1,6 @@ +import { FirebaseApp } from "@firebase/app"; +import {Code, FirestoreError} from "../util/error"; + /** * @license * Copyright 2017 Google LLC @@ -74,3 +77,14 @@ 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..8d19cfa5fc2 100644 --- a/packages/firestore/src/lite-api/database.ts +++ b/packages/firestore/src/lite-api/database.ts @@ -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 @@ -202,16 +182,55 @@ export function initializeFirestore( } /** - * Returns the existing `Firestore` instance that is associated with the + * 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. + * + * @returns The {@link Firestore} instance of the provided app. + */ +export function getFirestore(): 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 `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 1bcda165648..01c95477fb1 100644 --- a/packages/firestore/src/register.ts +++ b/packages/firestore/src/register.ts @@ -31,6 +31,7 @@ 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, @@ -43,13 +44,14 @@ export function registerFirestore( (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); diff --git a/packages/firestore/test/lite/integration.test.ts b/packages/firestore/test/lite/integration.test.ts index deaa9a0236f..ae1f21ff9e7 100644 --- a/packages/firestore/test/lite/integration.test.ts +++ b/packages/firestore/test/lite/integration.test.ts @@ -96,7 +96,7 @@ import { use(chaiAsPromised); -describe('Firestore', () => { +describe.only('Firestore', () => { it('can provide setting', () => { const app = initializeApp( { apiKey: 'fake-api-key', projectId: 'test-project' }, @@ -106,14 +106,57 @@ 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 = getFirestore(app); + const fs2 = getFirestore(app, 'name1'); + const fs3 = getFirestore(app, 'name2'); + expect(fs1).to.not.be.equal(fs2).and.equal(fs3); + expect(fs2).to.not.be.equal(fs3); + }); + + it('returns same default instance from default app', () => { + const app = initializeApp( + { apiKey: 'fake-api-key', projectId: 'test-project' } + ); + const fs1 = getFirestore(); + const fs2 = getFirestore(app); + const fs3 = getFirestore( '(default)'); + const fs4 = getFirestore(app, '(default)'); + expect(fs1).to.be.equal(fs2).and.equal(fs3).and.equal(fs4); + }); + + 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).and.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) ); } From 1546ce6b70bcc9fe9619c9f6e8585ac4b1cdcf26 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 8 Aug 2022 15:57:17 -0400 Subject: [PATCH 04/16] Fix linting --- packages/firestore/lite/register.ts | 10 ++++++---- packages/firestore/src/api/database.ts | 13 +++++++------ packages/firestore/src/core/database_info.ts | 10 +++++++--- packages/firestore/src/lite-api/database.ts | 13 +++++++------ packages/firestore/src/register.ts | 5 ++--- packages/firestore/test/lite/integration.test.ts | 15 +++++++-------- 6 files changed, 36 insertions(+), 30 deletions(-) diff --git a/packages/firestore/lite/register.ts b/packages/firestore/lite/register.ts index 2afb9bf0f29..a32bf761901 100644 --- a/packages/firestore/lite/register.ts +++ b/packages/firestore/lite/register.ts @@ -20,17 +20,19 @@ import { registerVersion, SDK_VERSION } from '@firebase/app'; -import { Component, ComponentType, InstanceFactoryOptions } from '@firebase/component'; +import { + Component, + ComponentType +} from '@firebase/component'; import { version } from '../package.json'; 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'; -import {databaseIdFromApp} from "../src/core/database_info"; declare module '@firebase/component' { interface NameServiceMapping { @@ -53,7 +55,7 @@ export function registerFirestore(): void { container.getProvider('app-check-internal') ), databaseIdFromApp(app, databaseId), - app, + app ); if (settings) { firestoreInstance._setSettings(settings); diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index ba87ee17c9b..5ad4b2ae465 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -211,16 +211,17 @@ export function getFirestore(databaseId: string): Firestore; * @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(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)'; + const app: FirebaseApp = + typeof appOrDatabaseId === 'object' ? appOrDatabaseId : getApp(); + const databaseId = + typeof appOrDatabaseId === 'string' + ? appOrDatabaseId + : optionalDatabaseId || '(default)'; return _getProvider(app, 'firestore').getImmediate({ identifier: databaseId }) as Firestore; diff --git a/packages/firestore/src/core/database_info.ts b/packages/firestore/src/core/database_info.ts index 4b0e334c5e6..1bee1c0c1c8 100644 --- a/packages/firestore/src/core/database_info.ts +++ b/packages/firestore/src/core/database_info.ts @@ -1,5 +1,6 @@ -import { FirebaseApp } from "@firebase/app"; -import {Code, FirestoreError} from "../util/error"; +import { FirebaseApp } from '@firebase/app'; + +import { Code, FirestoreError } from '../util/error'; /** * @license @@ -78,7 +79,10 @@ export class DatabaseId { } } -export function databaseIdFromApp(app: FirebaseApp, database?: string): DatabaseId { +export function databaseIdFromApp( + app: FirebaseApp, + database?: string +): DatabaseId { if (!Object.prototype.hasOwnProperty.apply(app.options, ['projectId'])) { throw new FirestoreError( Code.INVALID_ARGUMENT, diff --git a/packages/firestore/src/lite-api/database.ts b/packages/firestore/src/lite-api/database.ts index 8d19cfa5fc2..3d2b4fb0026 100644 --- a/packages/firestore/src/lite-api/database.ts +++ b/packages/firestore/src/lite-api/database.ts @@ -218,16 +218,17 @@ export function getFirestore(databaseId: string): Firestore; * @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(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)'; + 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; diff --git a/packages/firestore/src/register.ts b/packages/firestore/src/register.ts index 01c95477fb1..84ecc3d9746 100644 --- a/packages/firestore/src/register.ts +++ b/packages/firestore/src/register.ts @@ -30,8 +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"; +import { databaseIdFromApp } from './core/database_info'; export function registerFirestore( variant?: string, @@ -51,7 +50,7 @@ export function registerFirestore( container.getProvider('app-check-internal') ), databaseIdFromApp(app, databaseId), - app, + app ); settings = { useFetchStreams, ...settings }; firestoreInstance._setSettings(settings); diff --git a/packages/firestore/test/lite/integration.test.ts b/packages/firestore/test/lite/integration.test.ts index ae1f21ff9e7..e82458071a7 100644 --- a/packages/firestore/test/lite/integration.test.ts +++ b/packages/firestore/test/lite/integration.test.ts @@ -96,7 +96,7 @@ import { use(chaiAsPromised); -describe.only('Firestore', () => { +describe('Firestore', () => { it('can provide setting', () => { const app = initializeApp( { apiKey: 'fake-api-key', projectId: 'test-project' }, @@ -130,20 +130,19 @@ describe.only('Firestore', () => { }); it('returns same default instance from default app', () => { - const app = initializeApp( - { apiKey: 'fake-api-key', projectId: 'test-project' } - ); + const app = initializeApp({ + apiKey: 'fake-api-key', + projectId: 'test-project' + }); const fs1 = getFirestore(); const fs2 = getFirestore(app); - const fs3 = getFirestore( '(default)'); + const fs3 = getFirestore('(default)'); const fs4 = getFirestore(app, '(default)'); expect(fs1).to.be.equal(fs2).and.equal(fs3).and.equal(fs4); }); it('returns different instance from different named app', () => { - initializeApp( - { apiKey: 'fake-api-key', projectId: 'test-project' }, - ); + initializeApp({ apiKey: 'fake-api-key', projectId: 'test-project' }); const app1 = initializeApp( { apiKey: 'fake-api-key', projectId: 'test-project' }, 'test-app-getFirestore-1' From 36b774ff5ea1bd919972f7f1856b5d5ae216cfa0 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 8 Aug 2022 16:05:53 -0400 Subject: [PATCH 05/16] Fix linting --- packages/firestore/lite/register.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/firestore/lite/register.ts b/packages/firestore/lite/register.ts index a32bf761901..9bd7b014fa2 100644 --- a/packages/firestore/lite/register.ts +++ b/packages/firestore/lite/register.ts @@ -20,10 +20,7 @@ import { registerVersion, SDK_VERSION } from '@firebase/app'; -import { - Component, - ComponentType -} from '@firebase/component'; +import { Component, ComponentType } from '@firebase/component'; import { version } from '../package.json'; import { From 21be3483558630ee8abc4fd22723c6a015635545 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 8 Aug 2022 16:18:32 -0400 Subject: [PATCH 06/16] Undo package.json change. --- packages/firestore/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/firestore/package.json b/packages/firestore/package.json index 057c6f86f98..4f6614d808b 100644 --- a/packages/firestore/package.json +++ b/packages/firestore/package.json @@ -86,7 +86,6 @@ "@grpc/grpc-js": "^1.3.2", "@grpc/proto-loader": "^0.6.13", "node-fetch": "2.6.7", - "sqlite3": "^5.0.11", "tslib": "^2.1.0" }, "peerDependencies": { From 3aaa0ea5f7a6ab25380d6ec9d42f8dd6868b8ed4 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Tue, 9 Aug 2022 10:23:19 -0400 Subject: [PATCH 07/16] Changset for Multi-DB support --- .changeset/heavy-spiders-fry.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/heavy-spiders-fry.md 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 From 79e31d02b868eef18b1f1819e2e5aa46ba89063e Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 10 Aug 2022 21:17:21 -0400 Subject: [PATCH 08/16] Add more multi-db tests, and fix a few bugs related to multi-db. --- packages/firestore/src/api/database.ts | 36 ++-- packages/firestore/src/core/database_info.ts | 2 +- packages/firestore/src/lite-api/database.ts | 20 ++- packages/firestore/src/register.ts | 2 +- .../test/integration/api/database.test.ts | 64 ++++++- .../test/integration/api/provider.test.ts | 161 ++++++++++++++++++ .../test/integration/api/validation.test.ts | 13 +- .../test/integration/util/firebase_export.ts | 36 ++-- .../test/integration/util/helpers.ts | 32 +++- .../firestore/test/lite/integration.test.ts | 35 ++-- 10 files changed, 339 insertions(+), 62 deletions(-) create mode 100644 packages/firestore/test/integration/api/provider.test.ts diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index 5ad4b2ae465..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, @@ -136,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 { @@ -171,11 +180,14 @@ export function initializeFirestore( ); } - return provider.initialize({ options: settings }); + return provider.initialize({ + options: settings, + instanceIdentifier: databaseId + }); } /** - * 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. * @@ -183,7 +195,7 @@ export function initializeFirestore( */ export function getFirestore(): Firestore; /** - * Returns the existing {@link Firestore} instance that is associated with the + * 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. * @@ -221,7 +233,7 @@ export function getFirestore( const databaseId = typeof appOrDatabaseId === 'string' ? appOrDatabaseId - : optionalDatabaseId || '(default)'; + : optionalDatabaseId || DEFAULT_DATABASE_NAME; return _getProvider(app, 'firestore').getImmediate({ identifier: databaseId }) as Firestore; @@ -539,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 1bee1c0c1c8..306a9920ea9 100644 --- a/packages/firestore/src/core/database_info.ts +++ b/packages/firestore/src/core/database_info.ts @@ -50,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. diff --git a/packages/firestore/src/lite-api/database.ts b/packages/firestore/src/lite-api/database.ts index 3d2b4fb0026..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'; @@ -163,26 +163,34 @@ export class Firestore implements FirestoreService { * @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 {@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. * @@ -190,7 +198,7 @@ export function initializeFirestore( */ export function getFirestore(): Firestore; /** - * Returns the existing {@link Firestore} instance that is associated with the + * 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. * diff --git a/packages/firestore/src/register.ts b/packages/firestore/src/register.ts index 84ecc3d9746..3abb38c9d86 100644 --- a/packages/firestore/src/register.ts +++ b/packages/firestore/src/register.ts @@ -57,7 +57,7 @@ export function registerFirestore( 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..c39162ba858 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, + withNamedTestDbs } 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 withNamedTestDbs( + persistence, + ['db1', 'db2', 'db1'], + async ([db1, db2]) => { + const data = { name: 'Rafi', email: 'abc@xyz.com' }; + + const ref1 = await doc(collection(db1, 'users')); + 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')); + const snapshot2 = await getDoc(ref2); + expect(snapshot2.exists()).to.not.be.ok; + } + ); + }); + + it('can keep docs separate with multi-db when offline', () => { + return withNamedTestDbs( + persistence, + ['db1', 'db2', 'db1'], + 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..abcf2bc22b0 --- /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 '../../../src'; +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..9f778e00ebd 100644 --- a/packages/firestore/test/integration/util/helpers.ts +++ b/packages/firestore/test/integration/util/helpers.ts @@ -31,7 +31,8 @@ import { setDoc, PrivateSettings, SnapshotListenOptions, - newTestFirestore + newTestFirestore, + newTestApp } from './firebase_export'; import { ALT_PROJECT_ID, @@ -182,7 +183,34 @@ 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 withNamedTestDbs( + persistence: boolean, + dbNames: string[], + fn: (db: Firestore[]) => Promise +): Promise { + 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 e82458071a7..262a0489b05 100644 --- a/packages/firestore/test/lite/integration.test.ts +++ b/packages/firestore/test/lite/integration.test.ts @@ -122,11 +122,21 @@ describe('Firestore', () => { { apiKey: 'fake-api-key', projectId: 'test-project' }, 'test-app-getFirestore' ); - const fs1 = getFirestore(app); - const fs2 = getFirestore(app, 'name1'); - const fs3 = getFirestore(app, 'name2'); - expect(fs1).to.not.be.equal(fs2).and.equal(fs3); + 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', () => { @@ -134,11 +144,15 @@ describe('Firestore', () => { apiKey: 'fake-api-key', projectId: 'test-project' }); - const fs1 = getFirestore(); - const fs2 = getFirestore(app); - const fs3 = getFirestore('(default)'); - const fs4 = getFirestore(app, '(default)'); - expect(fs1).to.be.equal(fs2).and.equal(fs3).and.equal(fs4); + 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', () => { @@ -154,7 +168,8 @@ describe('Firestore', () => { const fs1 = getFirestore(); const fs2 = getFirestore(app1); const fs3 = getFirestore(app2); - expect(fs1).to.not.be.equal(fs2).and.equal(fs3); + expect(fs1).to.not.be.equal(fs2); + expect(fs1).to.not.be.equal(fs3); expect(fs2).to.not.be.equal(fs3); }); From 6291938a2fe436f3df7179e7ad544aec8b8374b5 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 10 Aug 2022 21:33:30 -0400 Subject: [PATCH 09/16] Expose API --- common/api-review/firestore-lite.api.md | 2 +- common/api-review/firestore.api.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/api-review/firestore-lite.api.md b/common/api-review/firestore-lite.api.md index c69f9d2da25..c220ad0810f 100644 --- a/common/api-review/firestore-lite.api.md +++ b/common/api-review/firestore-lite.api.md @@ -191,7 +191,7 @@ export function getFirestore(app: FirebaseApp, databaseId: string): Firestore; 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 f34e846eb7d..4e6e29b883b 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -243,7 +243,7 @@ export function getFirestore(app: FirebaseApp, databaseId: string): Firestore; 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; From a8b28d204410e8b43f80041eaff653248675d2ba Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 10 Aug 2022 22:03:27 -0400 Subject: [PATCH 10/16] Fix integration test --- integration/firestore/firebase_export.ts | 28 ++++++++++++++---------- 1 file changed, 17 insertions(+), 11 deletions(-) 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'; From 5f6159e55df9a0c4e0d8438a7bfcfd50c72c9bdf Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 10 Aug 2022 23:36:07 -0400 Subject: [PATCH 11/16] Fix import --- packages/firestore/test/integration/api/provider.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/firestore/test/integration/api/provider.test.ts b/packages/firestore/test/integration/api/provider.test.ts index abcf2bc22b0..ffcd0e7f350 100644 --- a/packages/firestore/test/integration/api/provider.test.ts +++ b/packages/firestore/test/integration/api/provider.test.ts @@ -26,7 +26,7 @@ import { Firestore, terminate, getDoc -} from '../../../src'; +} from '../util/firebase_export'; import { DEFAULT_SETTINGS } from '../util/settings'; use(chaiAsPromised); From 2f955f8bd1b47632b1b22f895ae6bdcb2e76b49b Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 11 Aug 2022 09:33:20 -0400 Subject: [PATCH 12/16] Fix test --- packages/firestore/test/integration/api/database.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index c39162ba858..244cd58bea0 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -1712,7 +1712,7 @@ apiDescribe('Database', (persistence: boolean) => { it('can keep docs separate with multi-db when online', () => { return withNamedTestDbs( persistence, - ['db1', 'db2', 'db1'], + ['db1', 'db2'], async ([db1, db2]) => { const data = { name: 'Rafi', email: 'abc@xyz.com' }; @@ -1732,7 +1732,7 @@ apiDescribe('Database', (persistence: boolean) => { it('can keep docs separate with multi-db when offline', () => { return withNamedTestDbs( persistence, - ['db1', 'db2', 'db1'], + ['db1', 'db2'], async ([db1, db2]) => { await disableNetwork(db1); await disableNetwork(db2); From ff7ec249d98c66746000d2dd6105f3b5a653f011 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 11 Aug 2022 09:43:12 -0400 Subject: [PATCH 13/16] Whitespace --- .../test/integration/api/database.test.ts | 62 ++++++++----------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index 244cd58bea0..5e2ac5cba11 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -1710,45 +1710,37 @@ apiDescribe('Database', (persistence: boolean) => { }); it('can keep docs separate with multi-db when online', () => { - return withNamedTestDbs( - persistence, - ['db1', 'db2'], - async ([db1, db2]) => { - const data = { name: 'Rafi', email: 'abc@xyz.com' }; + return withNamedTestDbs(persistence, ['db1', 'db2'], async ([db1, db2]) => { + const data = { name: 'Rafi', email: 'abc@xyz.com' }; - const ref1 = await doc(collection(db1, 'users')); - await setDoc(ref1, data); - const snapshot1 = await getDoc(ref1); - expect(snapshot1.exists()).to.be.ok; - expect(snapshot1.data()).to.be.deep.equals(data); + const ref1 = await doc(collection(db1, 'users')); + 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')); - const snapshot2 = await getDoc(ref2); - expect(snapshot2.exists()).to.not.be.ok; - } - ); + const ref2 = await doc(collection(db2, 'users')); + const snapshot2 = await getDoc(ref2); + expect(snapshot2.exists()).to.not.be.ok; + }); }); it('can keep docs separate with multi-db when offline', () => { - return withNamedTestDbs( - 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.' - ); - } - ); + return withNamedTestDbs(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.' + ); + }); }); }); From 05e813f37ff51c0135af1a8da0f8068851ebd723 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 11 Aug 2022 12:03:29 -0400 Subject: [PATCH 14/16] Only use emulator for integration tests. --- packages/firestore/test/integration/api/database.test.ts | 6 +++--- packages/firestore/test/integration/util/helpers.ts | 9 ++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index 5e2ac5cba11..18b290e936c 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -1713,14 +1713,14 @@ apiDescribe('Database', (persistence: boolean) => { return withNamedTestDbs(persistence, ['db1', 'db2'], async ([db1, db2]) => { const data = { name: 'Rafi', email: 'abc@xyz.com' }; - const ref1 = await doc(collection(db1, 'users')); + 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')); - const snapshot2 = await getDoc(ref2); + const ref2 = await doc(collection(db2, 'users'), 'doc1'); + const snapshot2 = await getDocFromServer(ref2); expect(snapshot2.exists()).to.not.be.ok; }); }); diff --git a/packages/firestore/test/integration/util/helpers.ts b/packages/firestore/test/integration/util/helpers.ts index 9f778e00ebd..791c27efe80 100644 --- a/packages/firestore/test/integration/util/helpers.ts +++ b/packages/firestore/test/integration/util/helpers.ts @@ -37,7 +37,8 @@ import { import { ALT_PROJECT_ID, DEFAULT_PROJECT_ID, - DEFAULT_SETTINGS + DEFAULT_SETTINGS, + USE_EMULATOR } from './settings'; /* eslint-disable no-restricted-globals */ @@ -207,6 +208,12 @@ export async function withNamedTestDbs( dbNames: string[], fn: (db: Firestore[]) => Promise ): Promise { + // Named DBs requires preparing project with DBs beforehand. + // Emulator does not DB to have been created beforehand. + // TODO: Improve integration testing to have more than just (default) available. + if (!USE_EMULATOR) { + return Promise.resolve(); + } const app = newTestApp(DEFAULT_PROJECT_ID); const dbs: Firestore[] = []; for (const dbName of dbNames) { From ab45518260e01c7c8e019ad294ddb5be891ba626 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 22 Aug 2022 11:07:18 -0400 Subject: [PATCH 15/16] Improve comments and function naming. --- packages/firestore/test/integration/api/database.test.ts | 6 +++--- packages/firestore/test/integration/util/helpers.ts | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index 18b290e936c..75125f1fdef 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -73,7 +73,7 @@ import { withTestDbs, withTestDoc, withTestDocAndInitialData, - withNamedTestDbs + withNamedTestDbsOrSkipUnlessUsingEmulator } from '../util/helpers'; import { DEFAULT_SETTINGS, DEFAULT_PROJECT_ID } from '../util/settings'; @@ -1710,7 +1710,7 @@ apiDescribe('Database', (persistence: boolean) => { }); it('can keep docs separate with multi-db when online', () => { - return withNamedTestDbs(persistence, ['db1', 'db2'], async ([db1, db2]) => { + return withNamedTestDbsOrSkipUnlessUsingEmulator(persistence, ['db1', 'db2'], async ([db1, db2]) => { const data = { name: 'Rafi', email: 'abc@xyz.com' }; const ref1 = await doc(collection(db1, 'users'), 'doc1'); @@ -1726,7 +1726,7 @@ apiDescribe('Database', (persistence: boolean) => { }); it('can keep docs separate with multi-db when offline', () => { - return withNamedTestDbs(persistence, ['db1', 'db2'], async ([db1, db2]) => { + return withNamedTestDbsOrSkipUnlessUsingEmulator(persistence, ['db1', 'db2'], async ([db1, db2]) => { await disableNetwork(db1); await disableNetwork(db2); const data = { name: 'Rafi', email: 'abc@xyz.com' }; diff --git a/packages/firestore/test/integration/util/helpers.ts b/packages/firestore/test/integration/util/helpers.ts index 791c27efe80..f2584916daf 100644 --- a/packages/firestore/test/integration/util/helpers.ts +++ b/packages/firestore/test/integration/util/helpers.ts @@ -203,14 +203,15 @@ export async function withTestDbsSettings( } } -export async function withNamedTestDbs( +export async function withNamedTestDbsOrSkipUnlessUsingEmulator( persistence: boolean, dbNames: string[], fn: (db: Firestore[]) => Promise ): Promise { - // Named DBs requires preparing project with DBs beforehand. - // Emulator does not DB to have been created beforehand. - // TODO: Improve integration testing to have more than just (default) available. + // 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(); } From 5c8a2fb4e0febb240852e74fbfac4aa5bca105b1 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 22 Aug 2022 11:15:06 -0400 Subject: [PATCH 16/16] Make pretty --- .../test/integration/api/database.test.ts | 62 +++++++++++-------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index 75125f1fdef..5f3e2dc6c61 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -1710,37 +1710,45 @@ 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' }; + 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 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; - }); + 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.' - ); - }); + 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.' + ); + } + ); }); });