Skip to content

Commit 050cc59

Browse files
authored
Using async project ID discovery in ProjectManagement and InstanceId (#728)
* Using async project ID discovery API in Auth and FCM * Async project ID discovery mechanism for ProjectManagement and InstanceId APIs
1 parent a18dc71 commit 050cc59

File tree

6 files changed

+140
-98
lines changed

6 files changed

+140
-98
lines changed

src/instance-id/instance-id-request.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
ApiSettings, AuthorizedHttpClient, HttpRequestConfig, HttpError,
2121
} from '../utils/api-request';
2222

23+
import * as utils from '../utils/index';
2324
import * as validator from '../utils/validator';
2425

2526
/** Firebase IID backend host. */
@@ -49,17 +50,15 @@ export class FirebaseInstanceIdRequestHandler {
4950
private readonly host: string = FIREBASE_IID_HOST;
5051
private readonly timeout: number = FIREBASE_IID_TIMEOUT;
5152
private readonly httpClient: AuthorizedHttpClient;
52-
private readonly path: string;
53+
private path: string;
5354

5455
/**
5556
* @param {FirebaseApp} app The app used to fetch access tokens to sign API requests.
56-
* @param {string} projectId A Firebase project ID string.
5757
*
5858
* @constructor
5959
*/
60-
constructor(app: FirebaseApp, projectId: string) {
60+
constructor(private readonly app: FirebaseApp) {
6161
this.httpClient = new AuthorizedHttpClient(app);
62-
this.path = FIREBASE_IID_PATH + `project/${projectId}/instanceId/`;
6362
}
6463

6564
public deleteInstanceId(instanceId: string): Promise<void> {
@@ -79,11 +78,10 @@ export class FirebaseInstanceIdRequestHandler {
7978
* @return {Promise<void>} A promise that resolves when the request is complete.
8079
*/
8180
private invokeRequestHandler(apiSettings: ApiSettings): Promise<void> {
82-
const path: string = this.path + apiSettings.getEndpoint();
83-
return Promise.resolve()
84-
.then(() => {
81+
return this.getPathPrefix()
82+
.then((path) => {
8583
const req: HttpRequestConfig = {
86-
url: `https://${this.host}${path}`,
84+
url: `https://${this.host}${path}${apiSettings.getEndpoint()}`,
8785
method: apiSettings.getHttpMethod(),
8886
timeout: this.timeout,
8987
};
@@ -107,4 +105,26 @@ export class FirebaseInstanceIdRequestHandler {
107105
throw err;
108106
});
109107
}
108+
109+
private getPathPrefix(): Promise<string> {
110+
if (this.path) {
111+
return Promise.resolve(this.path);
112+
}
113+
114+
return utils.findProjectId(this.app)
115+
.then((projectId) => {
116+
if (!validator.isNonEmptyString(projectId)) {
117+
// Assert for an explicit projct ID (either via AppOptions or the cert itself).
118+
throw new FirebaseInstanceIdError(
119+
InstanceIdClientErrorCode.INVALID_PROJECT_ID,
120+
'Failed to determine project ID for InstanceId. Initialize the '
121+
+ 'SDK with service account credentials or set project ID as an app option. '
122+
+ 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.',
123+
);
124+
}
125+
126+
this.path = FIREBASE_IID_PATH + `project/${projectId}/instanceId/`;
127+
return this.path;
128+
});
129+
}
110130
}

src/instance-id/instance-id.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {FirebaseInstanceIdError, InstanceIdClientErrorCode} from '../utils/error
1919
import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service';
2020
import {FirebaseInstanceIdRequestHandler} from './instance-id-request';
2121

22-
import * as utils from '../utils/index';
2322
import * as validator from '../utils/validator';
2423

2524
/**
@@ -55,19 +54,8 @@ export class InstanceId implements FirebaseServiceInterface {
5554
);
5655
}
5756

58-
const projectId: string | null = utils.getProjectId(app);
59-
if (!validator.isNonEmptyString(projectId)) {
60-
// Assert for an explicit projct ID (either via AppOptions or the cert itself).
61-
throw new FirebaseInstanceIdError(
62-
InstanceIdClientErrorCode.INVALID_PROJECT_ID,
63-
'Failed to determine project ID for InstanceId. Initialize the '
64-
+ 'SDK with service account credentials or set project ID as an app option. '
65-
+ 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.',
66-
);
67-
}
68-
6957
this.app_ = app;
70-
this.requestHandler = new FirebaseInstanceIdRequestHandler(app, projectId);
58+
this.requestHandler = new FirebaseInstanceIdRequestHandler(app);
7159
}
7260

7361
/**

src/project-management/project-management.ts

Lines changed: 97 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ class ProjectManagementInternals implements FirebaseServiceInternalsInterface {
4343
* ProjectManagement service bound to the provided app.
4444
*/
4545
export class ProjectManagement implements FirebaseServiceInterface {
46+
4647
public readonly INTERNAL: ProjectManagementInternals = new ProjectManagementInternals();
4748

48-
private readonly resourceName: string;
49-
private readonly projectId: string;
5049
private readonly requestHandler: ProjectManagementRequestHandler;
50+
private projectId: string;
5151

5252
/**
5353
* @param {object} app The app for this ProjectManagement service.
@@ -61,18 +61,6 @@ export class ProjectManagement implements FirebaseServiceInterface {
6161
+ 'instance.');
6262
}
6363

64-
// Assert that a specific project ID was provided within the app.
65-
const projectId = utils.getProjectId(app);
66-
if (!validator.isNonEmptyString(projectId)) {
67-
throw new FirebaseProjectManagementError(
68-
'invalid-project-id',
69-
'Failed to determine project ID. Initialize the SDK with service account credentials, or '
70-
+ 'set project ID as an app option. Alternatively, set the GOOGLE_CLOUD_PROJECT '
71-
+ 'environment variable.');
72-
}
73-
this.projectId = projectId;
74-
this.resourceName = `projects/${this.projectId}`;
75-
7664
this.requestHandler = new ProjectManagementRequestHandler(app);
7765
}
7866

@@ -115,56 +103,73 @@ export class ProjectManagement implements FirebaseServiceInterface {
115103
* Creates a new Firebase Android app, associated with this Firebase project.
116104
*/
117105
public createAndroidApp(packageName: string, displayName?: string): Promise<AndroidApp> {
118-
return this.requestHandler.createAndroidApp(this.resourceName, packageName, displayName)
119-
.then((responseData: any) => {
120-
assertServerResponse(
121-
validator.isNonNullObject(responseData),
122-
responseData,
123-
'createAndroidApp()\'s responseData must be a non-null object.');
106+
return this.getResourceName()
107+
.then((resourceName) => {
108+
return this.requestHandler.createAndroidApp(resourceName, packageName, displayName);
109+
})
110+
.then((responseData: any) => {
111+
assertServerResponse(
112+
validator.isNonNullObject(responseData),
113+
responseData,
114+
'createAndroidApp()\'s responseData must be a non-null object.');
124115

125-
assertServerResponse(
126-
validator.isNonEmptyString(responseData.appId),
127-
responseData,
128-
`"responseData.appId" field must be present in createAndroidApp()'s response data.`);
129-
return new AndroidApp(responseData.appId, this.requestHandler);
130-
});
116+
assertServerResponse(
117+
validator.isNonEmptyString(responseData.appId),
118+
responseData,
119+
`"responseData.appId" field must be present in createAndroidApp()'s response data.`);
120+
return new AndroidApp(responseData.appId, this.requestHandler);
121+
});
131122
}
132123

133124
/**
134125
* Creates a new Firebase iOS app, associated with this Firebase project.
135126
*/
136127
public createIosApp(bundleId: string, displayName?: string): Promise<IosApp> {
137-
return this.requestHandler.createIosApp(this.resourceName, bundleId, displayName)
138-
.then((responseData: any) => {
139-
assertServerResponse(
140-
validator.isNonNullObject(responseData),
141-
responseData,
142-
'createIosApp()\'s responseData must be a non-null object.');
128+
return this.getResourceName()
129+
.then((resourceName) => {
130+
return this.requestHandler.createIosApp(resourceName, bundleId, displayName);
131+
})
132+
.then((responseData: any) => {
133+
assertServerResponse(
134+
validator.isNonNullObject(responseData),
135+
responseData,
136+
'createIosApp()\'s responseData must be a non-null object.');
143137

144-
assertServerResponse(
145-
validator.isNonEmptyString(responseData.appId),
146-
responseData,
147-
`"responseData.appId" field must be present in createIosApp()'s response data.`);
148-
return new IosApp(responseData.appId, this.requestHandler);
149-
});
138+
assertServerResponse(
139+
validator.isNonEmptyString(responseData.appId),
140+
responseData,
141+
`"responseData.appId" field must be present in createIosApp()'s response data.`);
142+
return new IosApp(responseData.appId, this.requestHandler);
143+
});
150144
}
151145

152146
/**
153147
* Lists up to 100 Firebase apps associated with this Firebase project.
154148
*/
155149
public listAppMetadata(): Promise<AppMetadata[]> {
156-
return this.requestHandler.listAppMetadata(this.resourceName)
157-
.then((responseData) => this.transformResponseToAppMetadata(responseData));
150+
return this.getResourceName()
151+
.then((resourceName) => {
152+
return this.requestHandler.listAppMetadata(resourceName);
153+
})
154+
.then((responseData) => {
155+
return this.getProjectId()
156+
.then((projectId) => {
157+
return this.transformResponseToAppMetadata(responseData, projectId);
158+
});
159+
});
158160
}
159161

160162
/**
161163
* Update display name of the project
162164
*/
163165
public setDisplayName(newDisplayName: string): Promise<void> {
164-
return this.requestHandler.setDisplayName(this.resourceName, newDisplayName);
166+
return this.getResourceName()
167+
.then((resourceName) => {
168+
return this.requestHandler.setDisplayName(resourceName, newDisplayName);
169+
});
165170
}
166171

167-
private transformResponseToAppMetadata(responseData: any): AppMetadata[] {
172+
private transformResponseToAppMetadata(responseData: any, projectId: string): AppMetadata[] {
168173
this.assertListAppsResponseData(responseData, 'listAppMetadata()');
169174

170175
if (!responseData.apps) {
@@ -183,7 +188,7 @@ export class ProjectManagement implements FirebaseServiceInterface {
183188
const metadata: AppMetadata = {
184189
appId: appJson.appId,
185190
platform: (AppPlatform as any)[appJson.platform] || AppPlatform.PLATFORM_UNKNOWN,
186-
projectId: this.projectId,
191+
projectId,
187192
resourceName: appJson.name,
188193
};
189194
if (appJson.displayName) {
@@ -193,34 +198,63 @@ export class ProjectManagement implements FirebaseServiceInterface {
193198
});
194199
}
195200

201+
private getResourceName(): Promise<string> {
202+
return this.getProjectId()
203+
.then((projectId) => {
204+
return `projects/${projectId}`;
205+
});
206+
}
207+
208+
private getProjectId(): Promise<string> {
209+
if (this.projectId) {
210+
return Promise.resolve(this.projectId);
211+
}
212+
213+
return utils.findProjectId(this.app)
214+
.then((projectId) => {
215+
// Assert that a specific project ID was provided within the app.
216+
if (!validator.isNonEmptyString(projectId)) {
217+
throw new FirebaseProjectManagementError(
218+
'invalid-project-id',
219+
'Failed to determine project ID. Initialize the SDK with service account credentials, or '
220+
+ 'set project ID as an app option. Alternatively, set the GOOGLE_CLOUD_PROJECT '
221+
+ 'environment variable.');
222+
}
223+
224+
this.projectId = projectId;
225+
return this.projectId;
226+
});
227+
}
228+
196229
/**
197230
* Lists up to 100 Firebase apps for a specified platform, associated with this Firebase project.
198231
*/
199232
private listPlatformApps<T>(platform: 'android' | 'ios', callerName: string): Promise<T[]> {
200-
const listPromise: Promise<object> = (platform === 'android') ?
201-
this.requestHandler.listAndroidApps(this.resourceName)
202-
: this.requestHandler.listIosApps(this.resourceName);
233+
return this.getResourceName()
234+
.then((resourceName) => {
235+
return (platform === 'android') ?
236+
this.requestHandler.listAndroidApps(resourceName)
237+
: this.requestHandler.listIosApps(resourceName);
238+
})
239+
.then((responseData: any) => {
240+
this.assertListAppsResponseData(responseData, callerName);
203241

204-
return listPromise
205-
.then((responseData: any) => {
206-
this.assertListAppsResponseData(responseData, callerName);
242+
if (!responseData.apps) {
243+
return [];
244+
}
207245

208-
if (!responseData.apps) {
209-
return [];
246+
return responseData.apps.map((appJson: any) => {
247+
assertServerResponse(
248+
validator.isNonEmptyString(appJson.appId),
249+
responseData,
250+
`"apps[].appId" field must be present in the ${callerName} response data.`);
251+
if (platform === 'android') {
252+
return new AndroidApp(appJson.appId, this.requestHandler);
253+
} else {
254+
return new IosApp(appJson.appId, this.requestHandler);
210255
}
211-
212-
return responseData.apps.map((appJson: any) => {
213-
assertServerResponse(
214-
validator.isNonEmptyString(appJson.appId),
215-
responseData,
216-
`"apps[].appId" field must be present in the ${callerName} response data.`);
217-
if (platform === 'android') {
218-
return new AndroidApp(appJson.appId, this.requestHandler);
219-
} else {
220-
return new IosApp(appJson.appId, this.requestHandler);
221-
}
222-
});
223256
});
257+
});
224258
}
225259

226260
private assertListAppsResponseData(responseData: any, callerName: string): void {

test/unit/instance-id/instance-id-request.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ chai.use(chaiAsPromised);
3636
const expect = chai.expect;
3737

3838
describe('FirebaseInstanceIdRequestHandler', () => {
39-
const projectId: string = 'test-project-id';
39+
const projectId: string = 'project_id';
4040
const mockAccessToken: string = utils.generateRandomAccessToken();
4141
let stubs: sinon.SinonStub[] = [];
4242
let getTokenStub: sinon.SinonStub;
@@ -68,7 +68,7 @@ describe('FirebaseInstanceIdRequestHandler', () => {
6868
describe('Constructor', () => {
6969
it('should succeed with a FirebaseApp instance', () => {
7070
expect(() => {
71-
return new FirebaseInstanceIdRequestHandler(mockApp, projectId);
71+
return new FirebaseInstanceIdRequestHandler(mockApp);
7272
}).not.to.throw(Error);
7373
});
7474
});
@@ -84,7 +84,7 @@ describe('FirebaseInstanceIdRequestHandler', () => {
8484
.resolves(utils.responseFrom(''));
8585
stubs.push(stub);
8686

87-
const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp, projectId);
87+
const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp);
8888
return requestHandler.deleteInstanceId('test-iid')
8989
.then(() => {
9090
expect(stub).to.have.been.calledOnce.and.calledWith({
@@ -101,7 +101,7 @@ describe('FirebaseInstanceIdRequestHandler', () => {
101101
.rejects(utils.errorFrom({}, 404));
102102
stubs.push(stub);
103103

104-
const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp, projectId);
104+
const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp);
105105
return requestHandler.deleteInstanceId('test-iid')
106106
.then(() => {
107107
throw new Error('Unexpected success');
@@ -117,7 +117,7 @@ describe('FirebaseInstanceIdRequestHandler', () => {
117117
.rejects(utils.errorFrom({}, 409));
118118
stubs.push(stub);
119119

120-
const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp, projectId);
120+
const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp);
121121
return requestHandler.deleteInstanceId('test-iid')
122122
.then(() => {
123123
throw new Error('Unexpected success');
@@ -134,7 +134,7 @@ describe('FirebaseInstanceIdRequestHandler', () => {
134134
.rejects(utils.errorFrom(expectedResult, 511));
135135
stubs.push(stub);
136136

137-
const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp, projectId);
137+
const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp);
138138
return requestHandler.deleteInstanceId('test-iid')
139139
.then(() => {
140140
throw new Error('Unexpected success');

test/unit/instance-id/instance-id.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,13 @@ describe('InstanceId', () => {
9393
}).to.throw('First argument passed to admin.instanceId() must be a valid Firebase app instance.');
9494
});
9595

96-
it('should throw given an invalid credential without project ID', () => {
96+
it('should reject given an invalid credential without project ID', () => {
9797
// Project ID not set in the environment.
9898
delete process.env.GOOGLE_CLOUD_PROJECT;
9999
delete process.env.GCLOUD_PROJECT;
100-
expect(() => {
101-
return new InstanceId(mockCredentialApp);
102-
}).to.throw(noProjectIdError);
100+
const instanceId = new InstanceId(mockCredentialApp);
101+
return instanceId.deleteInstanceId('iid')
102+
.should.eventually.rejectedWith(noProjectIdError);
103103
});
104104

105105
it('should not throw given a valid app', () => {

0 commit comments

Comments
 (0)