diff --git a/common/api-review/firestore-lite.api.md b/common/api-review/firestore-lite.api.md index 603e2349505..4a9ef4c0171 100644 --- a/common/api-review/firestore-lite.api.md +++ b/common/api-review/firestore-lite.api.md @@ -494,4 +494,5 @@ export class WriteBatch { // @public export function writeBatch(firestore: Firestore): WriteBatch; + ``` diff --git a/packages/firestore/src/lite-api/database.ts b/packages/firestore/src/lite-api/database.ts index 9ea4d4ec52e..9a68e2a86d6 100644 --- a/packages/firestore/src/lite-api/database.ts +++ b/packages/firestore/src/lite-api/database.ts @@ -24,6 +24,7 @@ import { } from '@firebase/app'; import { createMockUserToken, + deepEqual, EmulatorMockTokenOptions, getDefaultEmulatorHostnameAndPort } from '@firebase/util'; @@ -71,6 +72,9 @@ export class Firestore implements FirestoreService { private _settings = new FirestoreSettingsImpl({}); private _settingsFrozen = false; + private _emulatorOptions: { + mockUserToken?: EmulatorMockTokenOptions | string; + } = {}; // A task that is assigned when the terminate() is invoked and resolved when // all components have shut down. Otherwise, Firestore is not terminated, @@ -119,6 +123,8 @@ export class Firestore implements FirestoreService { ); } this._settings = new FirestoreSettingsImpl(settings); + this._emulatorOptions = settings.emulatorOptions || {}; + if (settings.credentials !== undefined) { this._authCredentials = makeAuthCredentialsProvider(settings.credentials); } @@ -128,6 +134,10 @@ export class Firestore implements FirestoreService { return this._settings; } + _getEmulatorOptions(): { mockUserToken?: EmulatorMockTokenOptions | string } { + return this._emulatorOptions; + } + _freezeSettings(): FirestoreSettingsImpl { this._settingsFrozen = true; return this._settings; @@ -316,20 +326,30 @@ export function connectFirestoreEmulator( ): void { firestore = cast(firestore, Firestore); const settings = firestore._getSettings(); + const existingConfig = { + ...settings, + emulatorOptions: firestore._getEmulatorOptions() + }; const newHostSetting = `${host}:${port}`; - if (settings.host !== DEFAULT_HOST && settings.host !== newHostSetting) { logWarn( 'Host has been set in both settings() and connectFirestoreEmulator(), emulator host ' + 'will be used.' ); } - - firestore._setSettings({ + const newConfig = { ...settings, host: newHostSetting, - ssl: false - }); + ssl: false, + emulatorOptions: options + }; + // No-op if the new configuration matches the current configuration. This supports SSR + // enviornments which might call `connectFirestoreEmulator` multiple times as a standard practice. + if (deepEqual(newConfig, existingConfig)) { + return; + } + + firestore._setSettings(newConfig); if (options.mockUserToken) { let token: string; diff --git a/packages/firestore/src/lite-api/settings.ts b/packages/firestore/src/lite-api/settings.ts index 20551111a4f..a1bba373d13 100644 --- a/packages/firestore/src/lite-api/settings.ts +++ b/packages/firestore/src/lite-api/settings.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +import { EmulatorMockTokenOptions } from '@firebase/util'; + import { FirestoreLocalCache } from '../api/cache_config'; import { CredentialsSettings } from '../api/credentials'; import { @@ -80,6 +82,7 @@ export interface PrivateSettings extends FirestoreSettings { experimentalAutoDetectLongPolling?: boolean; experimentalLongPollingOptions?: ExperimentalLongPollingOptions; useFetchStreams?: boolean; + emulatorOptions?: { mockUserToken?: EmulatorMockTokenOptions | string }; localCache?: FirestoreLocalCache; } diff --git a/packages/firestore/test/integration/api/validation.test.ts b/packages/firestore/test/integration/api/validation.test.ts index 31f9d144142..9c74634affa 100644 --- a/packages/firestore/test/integration/api/validation.test.ts +++ b/packages/firestore/test/integration/api/validation.test.ts @@ -64,7 +64,9 @@ import { import { ALT_PROJECT_ID, DEFAULT_PROJECT_ID, - TARGET_DB_ID + TARGET_DB_ID, + USE_EMULATOR, + getEmulatorPort } from '../util/settings'; // We're using 'as any' to pass invalid values to APIs for testing purposes. @@ -179,7 +181,19 @@ apiDescribe('Validation:', persistence => { validationIt( persistence, - 'disallows calling connectFirestoreEmulator() after use', + 'connectFirestoreEmulator() can set mockUserToken object', + () => { + const db = newTestFirestore(newTestApp('test-project')); + // Verify that this doesn't throw. + connectFirestoreEmulator(db, '127.0.0.1', 9000, { + mockUserToken: { sub: 'foo' } + }); + } + ); + + validationIt( + persistence, + 'disallows calling connectFirestoreEmulator() for first time after use', async db => { const errorMsg = 'Firestore has already been started and its settings can no longer be changed.'; @@ -193,13 +207,33 @@ apiDescribe('Validation:', persistence => { validationIt( persistence, - 'connectFirestoreEmulator() can set mockUserToken object', - () => { - const db = newTestFirestore(newTestApp('test-project')); - // Verify that this doesn't throw. - connectFirestoreEmulator(db, '127.0.0.1', 9000, { - mockUserToken: { sub: 'foo' } - }); + 'allows calling connectFirestoreEmulator() after use with same config', + async db => { + if (USE_EMULATOR) { + const port = getEmulatorPort(); + connectFirestoreEmulator(db, '127.0.0.1', port); + await setDoc(doc(db, 'foo/bar'), {}); + expect(() => + connectFirestoreEmulator(db, '127.0.0.1', port) + ).to.not.throw(); + } + } + ); + + validationIt( + persistence, + 'disallows calling connectFirestoreEmulator() after use with different config', + async db => { + if (USE_EMULATOR) { + const errorMsg = + 'Firestore has already been started and its settings can no longer be changed.'; + const port = getEmulatorPort(); + connectFirestoreEmulator(db, '127.0.0.1', port); + await setDoc(doc(db, 'foo/bar'), {}); + expect(() => + connectFirestoreEmulator(db, '127.0.0.1', port + 1) + ).to.throw(errorMsg); + } } ); diff --git a/packages/firestore/test/integration/util/settings.ts b/packages/firestore/test/integration/util/settings.ts index 14bd4456c43..6fcb513a9a9 100644 --- a/packages/firestore/test/integration/util/settings.ts +++ b/packages/firestore/test/integration/util/settings.ts @@ -110,6 +110,10 @@ function getFirestoreHost(targetBackend: TargetBackend): string { } } +export function getEmulatorPort(): number { + return parseInt(process.env.FIRESTORE_EMULATOR_PORT || '8080', 10); +} + function getSslEnabled(targetBackend: TargetBackend): boolean { return targetBackend !== TargetBackend.EMULATOR; } diff --git a/packages/firestore/test/unit/api/database.test.ts b/packages/firestore/test/unit/api/database.test.ts index fd0e81cd05b..1cc1df51063 100644 --- a/packages/firestore/test/unit/api/database.test.ts +++ b/packages/firestore/test/unit/api/database.test.ts @@ -553,6 +553,17 @@ describe('Settings', () => { expect(db._getSettings().ssl).to.be.false; }); + it('gets privateSettings from useEmulator', () => { + // Use a new instance of Firestore in order to configure settings. + const db = newTestFirestore(); + const emulatorOptions = { mockUserToken: 'test' }; + connectFirestoreEmulator(db, '127.0.0.1', 9000, emulatorOptions); + + expect(db._getSettings().host).to.exist.and.to.equal('127.0.0.1:9000'); + expect(db._getSettings().ssl).to.exist.and.to.be.false; + expect(db._getEmulatorOptions()).to.equal(emulatorOptions); + }); + it('prefers host from useEmulator to host from settings', () => { // Use a new instance of Firestore in order to configure settings. const db = newTestFirestore();