Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions etc/firebase-admin.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,8 @@ export namespace messaging {
export type DataMessagePayload = DataMessagePayload;
// Warning: (ae-forgotten-export) The symbol "FcmOptions" needs to be exported by the entry point default-namespace.d.ts
export type FcmOptions = FcmOptions;
// Warning: (ae-forgotten-export) The symbol "FidMessage" needs to be exported by the entry point default-namespace.d.ts
export type FidMessage = FidMessage;
// Warning: (ae-forgotten-export) The symbol "LightSettings" needs to be exported by the entry point default-namespace.d.ts
export type LightSettings = LightSettings;
// Warning: (ae-forgotten-export) The symbol "Message" needs to be exported by the entry point default-namespace.d.ts
Expand All @@ -395,6 +397,8 @@ export namespace messaging {
// Warning: (ae-forgotten-export) The symbol "SendResponse" needs to be exported by the entry point default-namespace.d.ts
export type SendResponse = SendResponse;
// Warning: (ae-forgotten-export) The symbol "TokenMessage" needs to be exported by the entry point default-namespace.d.ts
//
// @deprecated
export type TokenMessage = TokenMessage;
// Warning: (ae-forgotten-export) The symbol "TopicMessage" needs to be exported by the entry point default-namespace.d.ts
export type TopicMessage = TopicMessage;
Expand Down
15 changes: 11 additions & 4 deletions etc/firebase-admin.messaging.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ export interface FcmOptions {
analyticsLabel?: string;
}

// @public (undocumented)
export interface FidMessage extends BaseMessage {
// (undocumented)
fid: string;
}

// Warning: (ae-forgotten-export) The symbol "PrefixedFirebaseError" needs to be exported by the entry point index.d.ts
//
// @public
Expand All @@ -187,7 +193,7 @@ export interface LightSettings {
}

// @public
export type Message = TokenMessage | TopicMessage | ConditionMessage;
export type Message = FidMessage | TokenMessage | TopicMessage | ConditionMessage;

// @public
export class Messaging {
Expand Down Expand Up @@ -329,8 +335,9 @@ export interface MessagingTopicManagementResponse {

// @public
export interface MulticastMessage extends BaseMessage {
// (undocumented)
tokens: string[];
fids?: string[];
// @deprecated
tokens?: string[];
}

// @public
Expand Down Expand Up @@ -366,7 +373,7 @@ export interface SendResponse {
success: boolean;
}

// @public (undocumented)
// @public @deprecated (undocumented)
export interface TokenMessage extends BaseMessage {
// (undocumented)
token: string;
Expand Down
1 change: 1 addition & 0 deletions src/messaging/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export {
CriticalSound,
ConditionMessage,
FcmOptions,
FidMessage,
LightSettings,
Message,
MessagingTopicManagementResponse,
Expand Down
25 changes: 21 additions & 4 deletions src/messaging/messaging-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ export interface BaseMessage {
fcmOptions?: FcmOptions;
}

export interface FidMessage extends BaseMessage {
fid: string;
}
Comment on lines +29 to +31
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To address your question in the PR description, it is highly recommended to add docstrings for the new FidMessage interface and its fields. This ensures consistency with other public interfaces and improves the developer experience for users of the SDK.

/**
 * Interface representing a message targeted at a Firebase Installation ID (FID).
 */
export interface FidMessage extends BaseMessage {
  /**
   * The Firebase Installation ID (FID) to which the message should be sent.
   */
  fid: string;
}


/**
* @deprecated Use {@link FidMessage} instead.
*/
export interface TokenMessage extends BaseMessage {
token: string;
}
Expand All @@ -40,16 +47,26 @@ export interface ConditionMessage extends BaseMessage {

/**
* Payload for the {@link Messaging.send} operation. The payload contains all the fields
* in the BaseMessage type, and exactly one of token, topic or condition.
* in the BaseMessage type, and exactly one of fid, token, topic or condition.
*/
export type Message = TokenMessage | TopicMessage | ConditionMessage;
export type Message = FidMessage | TokenMessage | TopicMessage | ConditionMessage;

/**
* Payload for the {@link Messaging.sendEachForMulticast} method. The payload contains all the fields
* in the BaseMessage type, and a list of tokens.
* in the BaseMessage type, and a list of tokens and/or fids.
*/
export interface MulticastMessage extends BaseMessage {
tokens: string[];
/**
* A list of Firebase Installation IDs (FIDs) to target.
*/
fids?: string[];

/**
* A list of registration tokens to target.
*
* @deprecated Use {@link MulticastMessage.fids} instead.
*/
tokens?: string[];
}
Comment on lines 58 to 70
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Changing tokens from a required field (tokens: string[]) to an optional one (tokens?: string[]) in the MulticastMessage interface is a breaking change for TypeScript consumers who might be relying on the property's presence (e.g., accessing .length without a null check). While necessary for the migration to fids, this should be clearly highlighted in the release notes.


/**
Expand Down
4 changes: 2 additions & 2 deletions src/messaging/messaging-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ export function validateMessage(message: Message): void {
}
}

const targets = [anyMessage.token, anyMessage.topic, anyMessage.condition];
const targets = [anyMessage.fid, anyMessage.token, anyMessage.topic, anyMessage.condition];
if (targets.filter((v) => validator.isNonEmptyString(v)).length !== 1) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_PAYLOAD,
'Exactly one of topic, token or condition is required');
'Exactly one of fid, topic, token or condition is required');
}

validateStringMap(message.data, 'data');
Expand Down
8 changes: 8 additions & 0 deletions src/messaging/messaging-namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
CriticalSound as TCriticalSound,
ConditionMessage as TConditionMessage,
FcmOptions as TFcmOptions,
FidMessage as TFidMessage,
LightSettings as TLightSettings,
Message as TMessage,
MessagingTopicManagementResponse as TMessagingTopicManagementResponse,
Expand Down Expand Up @@ -174,8 +175,15 @@ export namespace messaging {
*/
export type SendResponse = TSendResponse;

/**
* Type alias to {@link firebase-admin.messaging#FidMessage}.
*/
export type FidMessage = TFidMessage;

/**
* Type alias to {@link firebase-admin.messaging#TokenMessage}.
*
* @deprecated Use {@link firebase-admin.messaging#FidMessage} instead.
*/
export type TokenMessage = TTokenMessage;

Expand Down
39 changes: 33 additions & 6 deletions src/messaging/messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,27 +297,54 @@ export class Messaging {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT, 'MulticastMessage must be a non-null object');
}
if (!validator.isNonEmptyArray(copy.tokens)) {

const tokens: string[] = copy.tokens || [];
const fids: string[] = copy.fids || [];

if ('tokens' in copy && !validator.isNonEmptyArray(copy.tokens)) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT, 'tokens must be a non-empty array');
}
if (copy.tokens.length > FCM_MAX_BATCH_SIZE) {
if ('fids' in copy && !validator.isNonEmptyArray(copy.fids)) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT, 'fids must be a non-empty array');
}
if (tokens.length === 0 && fids.length === 0) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT, 'Either tokens or fids must be a non-empty array');
}

const totalLength = tokens.length + fids.length;
if (totalLength > FCM_MAX_BATCH_SIZE) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT,
`tokens list must not contain more than ${FCM_MAX_BATCH_SIZE} items`);
`tokens and fids list must not contain more than ${FCM_MAX_BATCH_SIZE} items in total`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error message grammar can be improved for better clarity.

Suggested change
`tokens and fids list must not contain more than ${FCM_MAX_BATCH_SIZE} items in total`);
`The total number of tokens and fids must not exceed ${FCM_MAX_BATCH_SIZE}.`);

}

const messages: Message[] = copy.tokens.map((token) => {
return {
const messages: Message[] = [];
tokens.forEach((token) => {
messages.push({
token,
android: copy.android,
apns: copy.apns,
data: copy.data,
notification: copy.notification,
webpush: copy.webpush,
fcmOptions: copy.fcmOptions,
};
});
});
fids.forEach((fid) => {
messages.push({
fid,
android: copy.android,
apns: copy.apns,
data: copy.data,
notification: copy.notification,
webpush: copy.webpush,
fcmOptions: copy.fcmOptions,
});
});
Comment on lines +324 to +346
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for constructing the messages array can be refactored to be more concise and future-proof by using object destructuring and the spread operator. This avoids manually listing every property of BaseMessage and automatically handles any future additions to the base type.

    const { tokens: _, fids: __, ...baseMessage } = copy;
    const messages: Message[] = [
      ...tokens.map((token) => ({ ...baseMessage, token } as Message)),
      ...fids.map((fid) => ({ ...baseMessage, fid } as Message)),
    ];


return this.sendEach(messages, dryRun);
}

Expand Down
53 changes: 53 additions & 0 deletions test/integration/messaging.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ const topic = 'mock-topic';

const invalidTopic = 'topic-$%#^';

const mockFid = 'mock-fid';

const message: Message = {
data: {
foo: 'bar',
Expand Down Expand Up @@ -80,6 +82,13 @@ const message: Message = {
topic: 'foo-bar',
};

const fidMessage: Message = {
data: message.data,
android: message.android,
apns: message.apns,
fid: mockFid,
};

describe('admin.messaging', () => {
it('send(message, dryRun) returns a message ID', () => {
return getMessaging().send(message, true)
Expand All @@ -88,6 +97,11 @@ describe('admin.messaging', () => {
});
});

it('send(message with fid, dryRun) fails with invalid-argument', () => {
return getMessaging().send(fidMessage, true)
.should.eventually.be.rejected.and.have.property('code', 'messaging/invalid-argument');
});

it('sendEach()', () => {
const messages: Message[] = [message, message, message];
return getMessaging().sendEach(messages, true)
Expand Down Expand Up @@ -138,6 +152,45 @@ describe('admin.messaging', () => {
});
});

it('sendEachForMulticast() with fids', () => {
const multicastMessage: MulticastMessage = {
data: message.data,
android: message.android,
fids: ['not-a-fid', 'also-not-a-fid'],
};
return getMessaging().sendEachForMulticast(multicastMessage, true)
.then((response) => {
expect(response.responses.length).to.equal(2);
expect(response.successCount).to.equal(0);
expect(response.failureCount).to.equal(2);
response.responses.forEach((resp) => {
expect(resp.success).to.be.false;
expect(resp.messageId).to.be.undefined;
expect(resp.error).to.have.property('code', 'messaging/invalid-argument');
});
});
});

it('sendEachForMulticast() with mixed tokens and fids', () => {
const multicastMessage: MulticastMessage = {
data: message.data,
android: message.android,
tokens: ['not-a-token'],
fids: ['not-a-fid'],
};
return getMessaging().sendEachForMulticast(multicastMessage, true)
.then((response) => {
expect(response.responses.length).to.equal(2);
expect(response.successCount).to.equal(0);
expect(response.failureCount).to.equal(2);
response.responses.forEach((resp) => {
expect(resp.success).to.be.false;
expect(resp.messageId).to.be.undefined;
expect(resp.error).to.have.property('code', 'messaging/invalid-argument');
});
});
});

it('subscribeToTopic() returns a response with success count', () => {
return getMessaging().subscribeToTopic(registrationToken, topic)
.then((response) => {
Expand Down
Loading
Loading