Skip to content

Commit 44d2779

Browse files
feat: preferRest option for Firestore
1 parent 1665b6e commit 44d2779

File tree

6 files changed

+173
-9
lines changed

6 files changed

+173
-9
lines changed

etc/firebase-admin.firestore.api.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ export { Firestore }
8282

8383
export { FirestoreDataConverter }
8484

85+
// @public
86+
export interface FirestoreSettings {
87+
preferRest?: boolean;
88+
}
89+
8590
export { GeoPoint }
8691

8792
// @public
@@ -94,6 +99,9 @@ export function getFirestore(app: App): Firestore;
9499

95100
export { GrpcStatus }
96101

102+
// @public
103+
export function initializeFirestore(app: App, settings?: FirestoreSettings): Firestore;
104+
97105
export { NestedUpdateFields }
98106

99107
export { OrderByDirection }

src/firestore/firestore-internal.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,46 @@ import * as validator from '../utils/validator';
2323
import * as utils from '../utils/index';
2424
import { App } from '../app';
2525

26+
/**
27+
* Settings to pass to the Firestore constructor.
28+
*
29+
* @public
30+
*/
31+
export interface FirestoreSettings {
32+
/**
33+
* Use HTTP/1.1 REST transport where possible.
34+
*
35+
* @defaultValue `undefined`
36+
*/
37+
preferRest?: boolean;
38+
}
39+
2640
export class FirestoreService {
2741

2842
private readonly appInternal: App;
2943
private readonly databases: Map<string, Firestore> = new Map();
44+
private readonly firestoreSettings: FirestoreSettings;
3045

31-
constructor(app: App) {
46+
constructor(app: App, firestoreSettings?: FirestoreSettings) {
3247
this.appInternal = app;
48+
this.firestoreSettings = firestoreSettings ?? {};
3349
}
3450

3551
getDatabase(databaseId: string): Firestore {
3652
let database = this.databases.get(databaseId);
3753
if (database === undefined) {
38-
database = initFirestore(this.app, databaseId);
54+
database = initFirestore(this.app, databaseId, this.firestoreSettings);
3955
this.databases.set(databaseId, database);
4056
}
4157
return database;
4258
}
4359

60+
checkIfSameSettings(firestoreSettings: FirestoreSettings): boolean {
61+
// If we start passing more settings to Firestore constructor,
62+
// replace this with deep equality check.
63+
return (firestoreSettings.preferRest === this.firestoreSettings.preferRest);
64+
}
65+
4466
/**
4567
* Returns the app associated with this Storage instance.
4668
*
@@ -51,7 +73,7 @@ export class FirestoreService {
5173
}
5274
}
5375

54-
export function getFirestoreOptions(app: App): Settings {
76+
export function getFirestoreOptions(app: App, firestoreSettings?: FirestoreSettings): Settings {
5577
if (!validator.isNonNullObject(app) || !('options' in app)) {
5678
throw new FirebaseFirestoreError({
5779
code: 'invalid-argument',
@@ -63,6 +85,7 @@ export function getFirestoreOptions(app: App): Settings {
6385
const credential = app.options.credential;
6486
// eslint-disable-next-line @typescript-eslint/no-var-requires
6587
const { version: firebaseVersion } = require('../../package.json');
88+
const preferRest = firestoreSettings?.preferRest;
6689
if (credential instanceof ServiceAccountCredential) {
6790
return {
6891
credentials: {
@@ -73,12 +96,15 @@ export function getFirestoreOptions(app: App): Settings {
7396
// guaranteed to be available.
7497
projectId: projectId!,
7598
firebaseVersion,
99+
preferRest,
76100
};
77101
} else if (isApplicationDefault(app.options.credential)) {
78102
// Try to use the Google application default credentials.
79103
// If an explicit project ID is not available, let Firestore client discover one from the
80104
// environment. This prevents the users from having to set GOOGLE_CLOUD_PROJECT in GCP runtimes.
81-
return validator.isNonEmptyString(projectId) ? { projectId, firebaseVersion } : { firebaseVersion };
105+
return validator.isNonEmptyString(projectId)
106+
? { projectId, firebaseVersion, preferRest }
107+
: { firebaseVersion, preferRest };
82108
}
83109

84110
throw new FirebaseFirestoreError({
@@ -89,8 +115,8 @@ export function getFirestoreOptions(app: App): Settings {
89115
});
90116
}
91117

92-
function initFirestore(app: App, databaseId: string): Firestore {
93-
const options = getFirestoreOptions(app);
118+
function initFirestore(app: App, databaseId: string, firestoreSettings?: FirestoreSettings): Firestore {
119+
const options = getFirestoreOptions(app, firestoreSettings);
94120
options.databaseId = databaseId;
95121
let firestoreDatabase: typeof Firestore;
96122
try {

src/firestore/index.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@
2323
import { Firestore } from '@google-cloud/firestore';
2424
import { App, getApp } from '../app';
2525
import { FirebaseApp } from '../app/firebase-app';
26-
import { FirestoreService } from './firestore-internal';
26+
import { FirestoreService, FirestoreSettings } from './firestore-internal';
2727
import { DEFAULT_DATABASE_ID } from '@google-cloud/firestore/build/src/path';
28+
import { FirebaseFirestoreError } from '../utils/error';
2829

2930
export {
3031
AddPrefixToKeys,
@@ -71,6 +72,8 @@ export {
7172
setLogFunction,
7273
} from '@google-cloud/firestore';
7374

75+
export { FirestoreSettings };
76+
7477
/**
7578
* Gets the {@link https://googleapis.dev/nodejs/firestore/latest/Firestore.html | Firestore}
7679
* service for the default app.
@@ -139,3 +142,58 @@ export function getFirestore(
139142
'firestore', (app) => new FirestoreService(app));
140143
return firestoreService.getDatabase(databaseId);
141144
}
145+
146+
/**
147+
* Gets the {@link https://googleapis.dev/nodejs/firestore/latest/Firestore.html | Firestore}
148+
* service for the given app, passing extra parameters to its constructor.
149+
*
150+
* @example
151+
* ```javascript
152+
* // Get the Firestore service for a specific app, require HTTP/1.1 REST transport
153+
* const otherFirestore = initializeFirestore(app, {preferRest: true});
154+
* ```
155+
*
156+
* @param App - whose `Firestore` service to
157+
* return. If not provided, the default `Firestore` service will be returned.
158+
*
159+
* @param settings - Settings object to be passed to the constructor.
160+
*
161+
* @returns The `Firestore` service associated with the provided app.
162+
*/
163+
export function initializeFirestore(app: App, settings?: FirestoreSettings): Firestore;
164+
165+
/**
166+
* @param app
167+
* @param settings
168+
* @param databaseId
169+
* @internal
170+
*/
171+
export function initializeFirestore(
172+
app: App,
173+
settings: FirestoreSettings,
174+
databaseId: string
175+
): Firestore;
176+
177+
export function initializeFirestore(
178+
app: App,
179+
settings?: FirestoreSettings,
180+
databaseId?: string
181+
): Firestore {
182+
settings ??= {};
183+
databaseId ??= DEFAULT_DATABASE_ID;
184+
const firebaseApp: FirebaseApp = app as FirebaseApp;
185+
const firestoreService = firebaseApp.getOrInitService(
186+
'firestore', (app) => new FirestoreService(app, settings));
187+
188+
if (!firestoreService.checkIfSameSettings(settings)) {
189+
throw new FirebaseFirestoreError({
190+
code: 'failed-precondition',
191+
message: 'initializeFirestore() has already been called with ' +
192+
'different options. To avoid this error, call initializeFirestore() with the ' +
193+
'same options as when it was originally called, or call getFirestore() to return the' +
194+
' already initialized instance.'
195+
});
196+
}
197+
198+
return firestoreService.getDatabase(databaseId);
199+
}

test/integration/firestore.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { clone } from 'lodash';
2020
import * as admin from '../../lib/index';
2121
import {
2222
DocumentReference, DocumentSnapshot, FieldValue, Firestore, FirestoreDataConverter,
23-
QueryDocumentSnapshot, Timestamp, getFirestore, setLogFunction,
23+
QueryDocumentSnapshot, Timestamp, getFirestore, initializeFirestore, setLogFunction,
2424
} from '../../lib/firestore/index';
2525

2626
chai.should();
@@ -47,6 +47,11 @@ describe('admin.firestore', () => {
4747
expect(firestore).to.not.be.undefined;
4848
});
4949

50+
it('initializeFirestore returns a Firestore client', () => {
51+
const firestore: Firestore = initializeFirestore(admin.app());
52+
expect(firestore).to.not.be.undefined;
53+
});
54+
5055
it('admin.firestore() returns a Firestore client', () => {
5156
const firestore: admin.firestore.Firestore = admin.firestore();
5257
expect(firestore).to.not.be.undefined;

test/unit/firestore/firestore.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,4 +200,16 @@ describe('Firestore', () => {
200200
});
201201
});
202202
});
203+
204+
describe('options.preferRest', () => {
205+
it('should not enable preferRest by default', () => {
206+
const options = getFirestoreOptions(mockApp);
207+
expect(options.preferRest).to.be.undefined;
208+
});
209+
210+
it('should enable preferRest if provided', () => {
211+
const options = getFirestoreOptions(mockApp, { preferRest: true });
212+
expect(options.preferRest).to.be.true;
213+
});
214+
});
203215
});

test/unit/firestore/index.spec.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import * as chaiAsPromised from 'chai-as-promised';
2323

2424
import * as mocks from '../../resources/mocks';
2525
import { App } from '../../../src/app/index';
26-
import { getFirestore, Firestore } from '../../../src/firestore/index';
26+
import { getFirestore, initializeFirestore, Firestore } from '../../../src/firestore/index';
2727
import { DEFAULT_DATABASE_ID } from '@google-cloud/firestore/build/src/path';
2828

2929
chai.should();
@@ -86,4 +86,59 @@ describe('Firestore', () => {
8686
expect(db1).to.not.equal(db2);
8787
});
8888
});
89+
90+
describe('initializeFirestore()', () => {
91+
it('should throw when default app is not available', () => {
92+
expect(() => {
93+
initializeFirestore(mockApp);
94+
}).to.throw('The default Firebase app does not exist.');
95+
});
96+
97+
it('should reject given an invalid credential without project ID', () => {
98+
// Project ID not set in the environment.
99+
delete process.env.GOOGLE_CLOUD_PROJECT;
100+
delete process.env.GCLOUD_PROJECT;
101+
expect(() => initializeFirestore(mockCredentialApp)).to.throw(noProjectIdError);
102+
});
103+
104+
it('should not throw given a valid app', () => {
105+
expect(() => {
106+
return initializeFirestore(mockApp);
107+
}).not.to.throw();
108+
});
109+
110+
it('should return the same instance for a given app instance', () => {
111+
const db1: Firestore = initializeFirestore(mockApp);
112+
const db2: Firestore = initializeFirestore(mockApp, {}, DEFAULT_DATABASE_ID);
113+
expect(db1).to.equal(db2);
114+
});
115+
116+
it('should return the same instance for a given app instance and databaseId', () => {
117+
const db1: Firestore = initializeFirestore(mockApp, {}, 'db');
118+
const db2: Firestore = initializeFirestore(mockApp, {}, 'db');
119+
expect(db1).to.equal(db2);
120+
});
121+
122+
it('should return the different instance for given same app instance, but different databaseId', () => {
123+
const db0: Firestore = initializeFirestore(mockApp, {}, DEFAULT_DATABASE_ID);
124+
const db1: Firestore = initializeFirestore(mockApp, {}, 'db1');
125+
const db2: Firestore = initializeFirestore(mockApp, {}, 'db2');
126+
expect(db0).to.not.equal(db1);
127+
expect(db0).to.not.equal(db2);
128+
expect(db1).to.not.equal(db2);
129+
});
130+
131+
it('getFirestore should return the same instance as initializeFirestore returned earlier', () => {
132+
const db1: Firestore = initializeFirestore(mockApp, {}, 'db');
133+
const db2: Firestore = getFirestore(mockApp, 'db');
134+
expect(db1).to.equal(db2);
135+
});
136+
137+
it('initializeFirestore should not allow create an instance with different settings', () => {
138+
initializeFirestore(mockApp, {}, 'db');
139+
expect(() => {
140+
return initializeFirestore(mockApp, { preferRest: true }, 'db');
141+
}).to.throw(/has already been called with different options/);
142+
});
143+
});
89144
});

0 commit comments

Comments
 (0)