Skip to content

Commit 3900ea9

Browse files
Fcm GAPIC type conversion (#1354)
This PR serves as a proof of concept to show that GAPIC generated clients can be integrated with Firebase Admin APIs. It also shows that this integration for pre-existing APIs requires a relatively substantial amount of work.
1 parent 1ffae15 commit 3900ea9

File tree

4 files changed

+191
-139
lines changed

4 files changed

+191
-139
lines changed

src/generated/messaging/src/v1/fcm_service_client.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ import jsonProtos = require('../../protos/protos.json');
3030
*/
3131
import * as gapicConfig from './fcm_service_client_config.json';
3232

33-
const version = require('../../../package.json').version;
33+
//! Temporary change, this modification should be avoidable with the suggested
34+
// directory refactoring.
35+
const version = require('../../../../../package.json').version;
3436

3537
/**
3638
* Firebase Cloud Messaging service (FCM) to target cross-platform messaging.

src/messaging/messaging-errors-internal.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,22 @@ import { HttpError } from '../utils/api-request';
1818
import { FirebaseMessagingError, MessagingClientErrorCode } from '../utils/error';
1919
import * as validator from '../utils/validator';
2020

21+
/**
22+
* Creates a new FirebaseMessagingError by extracting the error code, message, and
23+
* other relevant details from an error thrown by the generated GAPIC client.
24+
*
25+
* @param err
26+
* @returns
27+
*/
28+
export function createFirebaseErrorFromGapicError(err: Error): FirebaseMessagingError {
29+
const json = { error: err };
30+
31+
const errorCode = getErrorCode(json);
32+
const errorMessage = getErrorMessage(json);
33+
34+
return FirebaseMessagingError.fromServerError(errorCode, errorMessage, json);
35+
}
36+
2137
/**
2238
* Creates a new FirebaseMessagingError by extracting the error code, message and other relevant
2339
* details from an HTTP error response.

src/messaging/messaging.ts

Lines changed: 101 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@ import { validateMessage, BLACKLISTED_DATA_PAYLOAD_KEYS, BLACKLISTED_OPTIONS_KEY
2222
import { messaging } from './index';
2323
import { FirebaseMessagingRequestHandler } from './messaging-api-request-internal';
2424
import { ErrorInfo, MessagingClientErrorCode, FirebaseMessagingError } from '../utils/error';
25+
import { createFirebaseErrorFromGapicError } from './messaging-errors-internal';
26+
import { ServiceAccountCredential } from '../credential/credential-internal';
2527
import * as utils from '../utils';
2628
import * as validator from '../utils/validator';
29+
import * as protos from '../generated/messaging/protos/protos';
30+
import { FcmServiceClient } from '../generated/messaging/src/v1/fcm_service_client'
2731

2832
import MessagingInterface = messaging.Messaging;
2933
import Message = messaging.Message;
@@ -41,6 +45,10 @@ import MessagingConditionResponse = messaging.MessagingConditionResponse;
4145
import DataMessagePayload = messaging.DataMessagePayload;
4246
import NotificationMessagePayload = messaging.NotificationMessagePayload;
4347

48+
// GAPIC Generated client types
49+
import IMessage = protos.google.firebase.fcm.v1.IMessage;
50+
import ISendRequest = protos.google.firebase.fcm.v1.ISendMessageRequest;
51+
4452
/* eslint-disable @typescript-eslint/camelcase */
4553

4654
// FCM endpoints
@@ -193,6 +201,7 @@ export class Messaging implements MessagingInterface {
193201
private urlPath: string;
194202
private readonly appInternal: FirebaseApp;
195203
private readonly messagingRequestHandler: FirebaseMessagingRequestHandler;
204+
private fcmServiceClient: FcmServiceClient;
196205

197206
/**
198207
* Gets the {@link messaging.Messaging `Messaging`} service for the
@@ -228,6 +237,77 @@ export class Messaging implements MessagingInterface {
228237
return this.appInternal;
229238
}
230239

240+
241+
/**
242+
* Converts from the public {@link messaging.Message `Message`} type to the
243+
* internal/generated {@link protos.google.firebase.fcm.v1.Message `clientMessage`}
244+
* type.
245+
*
246+
* @param message The {@link messaging.Message `Message`} to be converted
247+
* @returns A {@link protos.google.firebase.fcm.v1.Message `clientMessage`},
248+
* where like fields are the same as the inptuted message.
249+
*/
250+
private convertToIMessage(message: Message): IMessage {
251+
//TODO: Add conversion for android, webpush, apns
252+
253+
const convertedMessage: IMessage = {
254+
data: message.data,
255+
notification: message.notification,
256+
fcmOptions: message.fcmOptions
257+
}
258+
259+
if ('token' in message) {
260+
convertedMessage.token = message.token;
261+
} else if ('topic' in message) {
262+
convertedMessage.topic = message.topic;
263+
} else if ('condition' in message) {
264+
convertedMessage.condition = message.condition;
265+
}
266+
267+
return convertedMessage;
268+
}
269+
270+
/**
271+
* Method that returns a fully instantiated service client.
272+
*
273+
* @returns an instantiated service client with either the application's
274+
* service account credentials or the default credentials
275+
*/
276+
private getFcmServiceClient(): Promise<FcmServiceClient> {
277+
if (this.fcmServiceClient) {
278+
return Promise.resolve(this.fcmServiceClient);
279+
}
280+
281+
const credential = this.app.options.credential;
282+
const options: { credentials?: object; fallback: 'rest'; projectId?: string } = {
283+
fallback: 'rest'
284+
};
285+
286+
if (credential instanceof ServiceAccountCredential) {
287+
options.credentials = {
288+
private_key: credential.privateKey,
289+
client_email: credential.clientEmail
290+
};
291+
}
292+
293+
return utils.findProjectId(this.appInternal).then(projectId => {
294+
if (!validator.isNonEmptyString(projectId)) {
295+
// Assert for an explicit project ID (either via AppOptions or the cert itself).
296+
throw new FirebaseMessagingError(
297+
MessagingClientErrorCode.INVALID_ARGUMENT,
298+
'Failed to determine project ID for Messaging. Initialize the '
299+
+ 'SDK with service account credentials or set project ID as an app option. '
300+
+ 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.',
301+
);
302+
}
303+
304+
options.projectId = projectId;
305+
}).then(() => {
306+
this.fcmServiceClient = new FcmServiceClient(options);
307+
return this.fcmServiceClient;
308+
})
309+
}
310+
231311
/**
232312
* Sends the given message via FCM.
233313
*
@@ -245,16 +325,27 @@ export class Messaging implements MessagingInterface {
245325
throw new FirebaseMessagingError(
246326
MessagingClientErrorCode.INVALID_ARGUMENT, 'dryRun must be a boolean');
247327
}
248-
return this.getUrlPath()
249-
.then((urlPath) => {
250-
const request: { message: Message; validate_only?: boolean } = { message: copy };
251-
if (dryRun) {
252-
request.validate_only = true;
253-
}
254-
return this.messagingRequestHandler.invokeRequestHandler(FCM_SEND_HOST, urlPath, request);
255-
})
256-
.then((response) => {
257-
return (response as any).name;
328+
329+
const IMessage = this.convertToIMessage(copy);
330+
331+
return this.getFcmServiceClient()
332+
.then(client => {
333+
return client.getProjectId()
334+
.then((projectId) => {
335+
const parent = `projects/${projectId}`;
336+
337+
const request: ISendRequest = {
338+
parent: parent,
339+
message: IMessage,
340+
validateOnly: dryRun
341+
};
342+
return client.sendMessage(request);
343+
})
344+
.then(([response]) => {
345+
return response.name!;
346+
}).catch(err => {
347+
throw createFirebaseErrorFromGapicError(err);
348+
});
258349
});
259350
}
260351

0 commit comments

Comments
 (0)