Skip to content

Commit ab7092f

Browse files
committed
fix: support firebase-admin v14
v14 removed the legacy namespaced API from the root firebase-admin entry point, so `firestore(...)` was undefined and makeDocumentSnapshot threw. Move to the modular subpaths (getFirestore, DocumentReference, GeoPoint, Timestamp from firebase-admin/firestore; App, deleteApp, initializeApp, AppOptions from firebase-admin/app). The DocumentReference branch of objectToValueProto read a `_referencePath` internal that no longer exists, emitting `projects//databases//<path>`. It now reads projectId and databaseId off the Firestore instance and includes the missing `documents` segment. The old spec assertion could not catch this because DocumentReference.toString() returned "[object Object]" on the bundled @google-cloud/firestore. Drops admin ^8 and ^9 from peerDependencies. The modular subpaths have no exports entry before v10, and this package has already imported firebase-admin/firestore since v3.
1 parent e97f6fb commit ab7092f

10 files changed

Lines changed: 64 additions & 49 deletions

File tree

‎package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"chai": "^4.2.0",
5353
"eslint": "^8.57.1",
5454
"eslint-config-prettier": "^10.1.8",
55-
"firebase-admin": "^12.0.0",
55+
"firebase-admin": "^14.3.0",
5656
"firebase-functions": "^4.9.0",
5757
"firebase-tools": "^13.15.4",
5858
"mocha": "^11.7.6",
@@ -61,7 +61,7 @@
6161
"typescript": "^5.5.4"
6262
},
6363
"peerDependencies": {
64-
"firebase-admin": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0",
64+
"firebase-admin": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0",
6565
"firebase-functions": ">=4.9.0",
6666
"jest": ">=28.0.0"
6767
},

‎spec/app.spec.ts‎

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
import { expect } from 'chai';
2424
import * as sinon from 'sinon';
25-
import * as firebase from 'firebase-admin';
25+
import * as adminApp from 'firebase-admin/app';
2626

2727
import { testApp } from '../src/app';
2828
import { FirebaseFunctionsTest } from '../src/lifecycle';
@@ -42,34 +42,27 @@ describe('app', () => {
4242
});
4343

4444
describe('#getApp', () => {
45-
const spy = sinon.spy(firebase, 'initializeApp');
46-
4745
afterEach(() => {
48-
spy.resetHistory();
4946
appInstance.deleteApp();
5047
});
5148

5249
it('should initialize a new app if appSingleton does not exist', () => {
53-
appInstance.getApp();
54-
expect(spy.called).to.be.true;
50+
const app = appInstance.getApp();
51+
expect(app.name).to.equal('firebase-functions-test');
5552
});
5653

5754
it('should only initialize app once', () => {
58-
appInstance.getApp();
59-
appInstance.getApp();
60-
expect(spy.calledOnce).to.be.true;
55+
expect(appInstance.getApp()).to.equal(appInstance.getApp());
6156
});
6257
});
6358

6459
describe('#deleteApp', () => {
6560
it('deletes appSingleton if it exists', () => {
66-
const spy = sinon.spy();
67-
appInstance.appSingleton = {
68-
delete: spy,
69-
};
61+
const app = appInstance.getApp();
62+
expect(adminApp.getApps()).to.include(app);
7063
appInstance.deleteApp();
71-
expect(spy.called).to.be.true;
7264
expect(appInstance.appSingleton).to.equal(undefined);
65+
expect(adminApp.getApps()).to.not.include(app);
7366
});
7467

7568
it('does not throw an error if there are no apps to delete', () => {

‎spec/providers/firestore.spec.ts‎

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { expect } from 'chai';
2-
import * as firebase from 'firebase-admin';
2+
import { deleteApp, initializeApp } from 'firebase-admin/app';
3+
import {
4+
DocumentReference,
5+
GeoPoint,
6+
getFirestore,
7+
Timestamp,
8+
} from 'firebase-admin/firestore';
39
import * as sinon from 'sinon';
410
import * as http from 'http';
511
import { FeaturesList } from '../../src/features';
@@ -51,7 +57,7 @@ describe('providers/firestore', () => {
5157
});
5258

5359
it('should allow geopoints with makeDocumentSnapshot', () => {
54-
const hq = new firebase.firestore.GeoPoint(47.6703, 122.1971);
60+
const hq = new GeoPoint(47.6703, 122.1971);
5561
const snapshot = test.firestore.makeDocumentSnapshot(
5662
{ geopoint: hq },
5763
'collection/doc-id'
@@ -67,29 +73,30 @@ describe('providers/firestore', () => {
6773
'collection/doc-id'
6874
);
6975

70-
expect(snapshot.data().time).to.be.instanceof(firebase.firestore.Timestamp);
76+
expect(snapshot.data().time).to.be.instanceof(Timestamp);
7177
expect(snapshot.data().time.toDate()).to.deep.equal(time);
7278
});
7379

7480
it('should allow references with makeDocumentSnapshot', () => {
75-
firebase.initializeApp({
81+
initializeApp({
7682
projectId: 'not-a-project',
7783
});
7884

79-
const ref = firebase.firestore().doc('collection/doc-id');
85+
const ref = getFirestore().doc('collection/doc-id');
8086
const snapshot = test.firestore.makeDocumentSnapshot(
8187
{ ref },
8288
'collection/doc-id'
8389
);
8490

85-
expect(snapshot.data().ref).to.be.instanceOf(
86-
firebase.firestore.DocumentReference
91+
expect(snapshot.data().ref).to.be.instanceOf(DocumentReference);
92+
expect(snapshot.data().ref.path).to.equal(ref.path);
93+
expect(snapshot.data().ref.formattedName).to.equal(
94+
'projects/not-a-project/databases/(default)/documents/collection/doc-id'
8795
);
88-
expect(snapshot.data().ref.toString()).to.equal(ref.toString());
8996
});
9097

9198
it('should use firebaseApp projectId in makeDocumentSnapshot options', async () => {
92-
const customApp = firebase.initializeApp(
99+
const customApp = initializeApp(
93100
{
94101
projectId: 'custom-project',
95102
},
@@ -112,7 +119,7 @@ describe('providers/firestore', () => {
112119
'projects/custom-project/databases/(default)/documents/collection/doc-id'
113120
);
114121
} finally {
115-
await customApp.delete();
122+
deleteApp(customApp);
116123
}
117124
});
118125

‎src/app.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121
// SOFTWARE.
2222

23-
import * as firebase from 'firebase-admin';
23+
import { App as AdminApp, deleteApp, initializeApp } from 'firebase-admin/app';
2424

2525
/** @internal */
2626
export function testApp(): testApp.App {
@@ -36,15 +36,15 @@ export namespace testApp {
3636
export let init = () => (singleton = new testApp.App());
3737

3838
export class App {
39-
appSingleton: firebase.app.App;
39+
appSingleton: AdminApp;
4040
constructor() {}
4141

42-
getApp(): firebase.app.App {
42+
getApp(): AdminApp {
4343
if (typeof this.appSingleton === 'undefined') {
4444
const config = process.env.FIREBASE_CONFIG
4545
? JSON.parse(process.env.FIREBASE_CONFIG)
4646
: {};
47-
this.appSingleton = firebase.initializeApp(
47+
this.appSingleton = initializeApp(
4848
config,
4949
// Give this app a name so it does not conflict with apps that user initialized.
5050
'firebase-functions-test'
@@ -55,7 +55,7 @@ export namespace testApp {
5555

5656
deleteApp() {
5757
if (this.appSingleton) {
58-
this.appSingleton.delete();
58+
deleteApp(this.appSingleton);
5959
delete this.appSingleton;
6060
}
6161
}

‎src/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121
// SOFTWARE.
2222

23-
import { AppOptions } from 'firebase-admin';
23+
import { AppOptions } from 'firebase-admin/app';
2424
import { merge } from 'lodash';
2525

2626
import { FirebaseFunctionsTest } from './lifecycle';

‎src/lifecycle.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
// SOFTWARE.
2222

2323
import { isEmpty } from 'lodash';
24-
import { AppOptions } from 'firebase-admin';
24+
import { AppOptions } from 'firebase-admin/app';
2525
import { forEach } from 'lodash';
2626

2727
import { testApp } from './app';

‎src/providers/database.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
// SOFTWARE.
2222

2323
import { database, Change } from 'firebase-functions/v1';
24-
import { app } from 'firebase-admin';
24+
import { App } from 'firebase-admin/app';
2525

2626
import { testApp } from '../app';
2727

@@ -36,7 +36,7 @@ export function makeDataSnapshot(
3636
* You do not need to supply this parameter if you supplied Firebase config values when initializing
3737
* firebase-functions-test.
3838
*/
39-
firebaseApp?: app.App,
39+
firebaseApp?: App,
4040
/**
4141
* The RTDB instance to use when creating snapshot. This will override the `firebaseApp` parameter.
4242
* If omitted the default RTDB instance is used.

‎src/providers/firestore.ts‎

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,21 @@
2121
// SOFTWARE.
2222

2323
import { Change } from 'firebase-functions/v1';
24-
import { firestore, app } from 'firebase-admin';
2524
import { has, get, isEmpty, isPlainObject, mapValues } from 'lodash';
2625
import { inspect } from 'util';
2726

2827
import { testApp } from '../app';
2928

3029
import * as http from 'http';
3130
import {
31+
DocumentReference,
3232
DocumentSnapshot,
33+
GeoPoint,
34+
getFirestore,
3335
QueryDocumentSnapshot,
36+
Timestamp,
3437
} from 'firebase-admin/firestore';
38+
import { App } from 'firebase-admin/app';
3539

3640
function dateToTimestampProto(
3741
timeString?: string
@@ -61,7 +65,7 @@ export interface DocumentSnapshotOptions {
6165
/** The Firebase app that the Firestore database belongs to. You do not need to supply
6266
* this parameter if you supplied Firebase config values when initializing firebase-functions-test.
6367
*/
64-
firebaseApp?: app.App;
68+
firebaseApp?: App;
6569
}
6670

6771
/** Create a DocumentSnapshot. */
@@ -77,10 +81,10 @@ export function makeDocumentSnapshot(
7781
let firestoreService;
7882
let project;
7983
if (options?.firebaseApp) {
80-
firestoreService = firestore(options.firebaseApp);
84+
firestoreService = getFirestore(options.firebaseApp);
8185
project = options.firebaseApp.options.projectId;
8286
} else {
83-
firestoreService = firestore(testApp().getApp());
87+
firestoreService = getFirestore(testApp().getApp());
8488
project = process.env.GCLOUD_PROJECT;
8589
}
8690

@@ -107,7 +111,7 @@ export function makeDocumentSnapshot(
107111
/** Fetch an example document snapshot already populated with data. Can be passed into a wrapped
108112
* Firestore onCreate or onDelete function.
109113
*/
110-
export function exampleDocumentSnapshot(): firestore.DocumentSnapshot {
114+
export function exampleDocumentSnapshot(): DocumentSnapshot {
111115
return makeDocumentSnapshot(
112116
{
113117
aString: 'foo',
@@ -124,7 +128,7 @@ export function exampleDocumentSnapshot(): firestore.DocumentSnapshot {
124128
/** Fetch an example Change object of document snapshots already populated with data.
125129
* Can be passed into a wrapped Firestore onUpdate or onWrite function.
126130
*/
127-
export function exampleDocumentSnapshotChange(): Change<firestore.DocumentSnapshot> {
131+
export function exampleDocumentSnapshotChange(): Change<DocumentSnapshot> {
128132
return Change.fromObjects(
129133
makeDocumentSnapshot(
130134
{
@@ -205,24 +209,33 @@ export function objectToValueProto(data: object) {
205209
bytesValue: val,
206210
};
207211
}
208-
if (val instanceof firestore.DocumentReference) {
209-
const projectId: string = get(val, '_referencePath.projectId');
210-
const database: string = get(val, '_referencePath.databaseId');
212+
if (val instanceof DocumentReference) {
213+
// projectId and databaseId live on the Firestore instance, not on the
214+
// reference, and neither has a public accessor across the supported
215+
// firebase-admin range.
216+
const projectId: string =
217+
get(val, 'firestore.projectId') ||
218+
get(val, 'firestore._settings.projectId');
219+
const databaseId: string =
220+
get(val, 'firestore.databaseId') ||
221+
get(val, 'firestore._settings.databaseId') ||
222+
'(default)';
211223
const referenceValue: string = [
212224
'projects',
213225
projectId,
214226
'databases',
215-
database,
227+
databaseId,
228+
'documents',
216229
val.path,
217230
].join('/');
218231
return { referenceValue };
219232
}
220-
if (val instanceof firestore.Timestamp) {
233+
if (val instanceof Timestamp) {
221234
return {
222235
timestampValue: val.toDate().toISOString(),
223236
};
224237
}
225-
if (val instanceof firestore.GeoPoint) {
238+
if (val instanceof GeoPoint) {
226239
return {
227240
geoPointValue: {
228241
latitude: val.latitude,

‎tsconfig.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"outDir": ".tmp",
77
"sourceMap": true,
88
"target": "es6",
9-
"typeRoots": ["node_modules/@types"]
9+
"typeRoots": ["node_modules/@types"],
10+
"skipLibCheck": true
1011
},
1112
"include": ["src/**/*.ts", "spec/**/*.ts"]
1213
}

‎tsconfig.release.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"outDir": "lib",
88
"stripInternal": true,
99
"target": "es6",
10-
"typeRoots": ["node_modules/@types"]
10+
"typeRoots": ["node_modules/@types"],
11+
"skipLibCheck": true
1112
},
1213
"files": ["src/index.ts"]
1314
}

0 commit comments

Comments
 (0)