From b9d4b3a4087ddca5f4655db8ae03b7000541053d Mon Sep 17 00:00:00 2001 From: umair Date: Fri, 10 Apr 2026 11:12:11 +0100 Subject: [PATCH 1/2] Allows sending a realtime message and a push message when using push commands targeting a channel --- README.md | 12 ++- src/commands/push/publish.ts | 43 ++++++++- test/unit/commands/push/publish.test.ts | 123 ++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8bd2c775..a0910c2d 100644 --- a/README.md +++ b/README.md @@ -3581,9 +3581,9 @@ Publish a push notification to a device, client, or channel ``` USAGE $ ably push publish [-v] [--json | --pretty-json] [--device-id | --client-id | --recipient - ] [--channel ] [--title ] [--body ] [--sound ] [--icon ] [--badge ] - [--data ] [--collapse-key ] [--ttl ] [--payload ] [--apns ] [--fcm ] - [--web ] [-f] + ] [--channel ] [--message ] [--title ] [--body ] [--sound ] [--icon + ] [--badge ] [--data ] [--collapse-key ] [--ttl ] [--payload ] [--apns + ] [--fcm ] [--web ] [-f] FLAGS -f, --force Skip confirmation prompt (required with --json) @@ -3600,6 +3600,8 @@ FLAGS --fcm= FCM-specific override as JSON --icon= Notification icon --json Output in JSON format + --message= Realtime message data to include alongside the push notification (only applies when + publishing via --channel) --payload= Full notification payload as JSON (overrides convenience flags) --pretty-json Output in colorized JSON format --recipient= Raw recipient JSON for advanced targeting @@ -3632,6 +3634,10 @@ EXAMPLES $ ably push publish --channel my-channel --payload ./notification.json + $ ably push publish --channel my-channel --title Hello --body World --message 'Hello from push' + + $ ably push publish --channel my-channel --title Hello --body World --message '{"event":"push","text":"Hello"}' + $ ably push publish --recipient '{"transportType":"apns","deviceToken":"token123"}' --title Hello --body World $ ably push publish --device-id device-123 --title Hello --body World --json diff --git a/src/commands/push/publish.ts b/src/commands/push/publish.ts index f22d57bb..6f31b0af 100644 --- a/src/commands/push/publish.ts +++ b/src/commands/push/publish.ts @@ -23,6 +23,8 @@ export default class PushPublish extends AblyBaseCommand { '<%= config.bin %> <%= command.id %> --channel my-channel --title Hello --body World --data \'{"key":"value"}\'', '<%= config.bin %> <%= command.id %> --channel my-channel --payload \'{"notification":{"title":"Hello","body":"World"},"data":{"key":"value"}}\'', "<%= config.bin %> <%= command.id %> --channel my-channel --payload ./notification.json", + "<%= config.bin %> <%= command.id %> --channel my-channel --title Hello --body World --message 'Hello from push'", + '<%= config.bin %> <%= command.id %> --channel my-channel --title Hello --body World --message \'{"event":"push","text":"Hello"}\'', '<%= config.bin %> <%= command.id %> --recipient \'{"transportType":"apns","deviceToken":"token123"}\' --title Hello --body World', "<%= config.bin %> <%= command.id %> --device-id device-123 --title Hello --body World --json", ]; @@ -45,6 +47,10 @@ export default class PushPublish extends AblyBaseCommand { description: "Target channel name (publishes push notification via the channel using extras.push; ignored if --device-id, --client-id, or --recipient is also provided)", }), + message: Flags.string({ + description: + "Realtime message data to include alongside the push notification (only applies when publishing via --channel)", + }), title: Flags.string({ description: "Notification title", }), @@ -91,6 +97,14 @@ export default class PushPublish extends AblyBaseCommand { const hasDirectRecipient = flags["device-id"] || flags["client-id"] || flags.recipient; + if (flags.message && !flags.channel) { + this.fail( + "--message can only be used with --channel (realtime message data is not applicable when publishing directly to a device or client)", + flags as BaseFlags, + "pushPublish", + ); + } + if (!hasDirectRecipient && !flags.channel) { this.fail( "A target is required: --device-id, --client-id, --recipient, or --channel", @@ -104,6 +118,12 @@ export default class PushPublish extends AblyBaseCommand { "--channel is ignored when --device-id, --client-id, or --recipient is provided.", flags as BaseFlags, ); + if (flags.message) { + this.logWarning( + "--message is ignored when --device-id, --client-id, or --recipient is provided.", + flags as BaseFlags, + ); + } } try { @@ -257,13 +277,28 @@ export default class PushPublish extends AblyBaseCommand { } } - await rest.channels - .get(channelName) - .publish({ extras: { push: payload } }); + const publishMessage: Record = { + extras: { push: payload }, + }; + if (flags.message) { + try { + publishMessage.data = JSON.parse(flags.message); + } catch { + publishMessage.data = flags.message; + } + } + + await rest.channels.get(channelName).publish(publishMessage); if (this.shouldOutputJson(flags)) { this.logJsonResult( - { notification: { published: true, channel: channelName } }, + { + notification: { + published: true, + channel: channelName, + ...(flags.message ? { messageData: true } : {}), + }, + }, flags, ); } diff --git a/test/unit/commands/push/publish.test.ts b/test/unit/commands/push/publish.test.ts index 5a552d6e..44b8b41d 100644 --- a/test/unit/commands/push/publish.test.ts +++ b/test/unit/commands/push/publish.test.ts @@ -35,6 +35,7 @@ describe("push:publish command", () => { "--title", "--body", "--payload", + "--message", ]); describe("functionality", () => { @@ -197,6 +198,128 @@ describe("push:publish command", () => { }); }); + describe("--message flag (realtime message data)", () => { + it("should include string message data when publishing via channel", async () => { + const mock = getMockAblyRest(); + const channel = mock.channels._getChannel("my-channel"); + + await runCommand( + [ + "push:publish", + "--channel", + "my-channel", + "--title", + "Hello", + "--message", + "hello-world", + "--force", + ], + import.meta.url, + ); + + expect(channel.publish).toHaveBeenCalledWith( + expect.objectContaining({ + data: "hello-world", + extras: { + push: expect.objectContaining({ + notification: expect.objectContaining({ title: "Hello" }), + }), + }, + }), + ); + }); + + it("should parse JSON message data when publishing via channel", async () => { + const mock = getMockAblyRest(); + const channel = mock.channels._getChannel("my-channel"); + + await runCommand( + [ + "push:publish", + "--channel", + "my-channel", + "--title", + "Hello", + "--message", + '{"key":"val"}', + "--force", + ], + import.meta.url, + ); + + expect(channel.publish).toHaveBeenCalledWith( + expect.objectContaining({ + data: { key: "val" }, + extras: { + push: expect.objectContaining({ + notification: expect.objectContaining({ title: "Hello" }), + }), + }, + }), + ); + }); + + it("should fail when --message is used without --channel", async () => { + const { error } = await runCommand( + ["push:publish", "--message", "hello", "--title", "Hi"], + import.meta.url, + ); + + expect(error).toBeDefined(); + expect(error?.message).toContain( + "--message can only be used with --channel", + ); + }); + + it("should ignore --message when direct recipient overrides --channel", async () => { + const mock = getMockAblyRest(); + + const { stdout, stderr } = await runCommand( + [ + "push:publish", + "--device-id", + "dev-1", + "--channel", + "my-channel", + "--message", + "hello", + "--title", + "Hi", + ], + import.meta.url, + ); + + expect(stdout + stderr).toContain("--message is ignored"); + expect(mock.push.admin.publish).toHaveBeenCalledWith( + { deviceId: "dev-1" }, + expect.anything(), + ); + }); + + it("should include messageData in JSON output when --message is used", async () => { + const { stdout } = await runCommand( + [ + "push:publish", + "--channel", + "my-channel", + "--title", + "Hi", + "--message", + "hello", + "--json", + "--force", + ], + import.meta.url, + ); + + const result = parseJsonOutput(stdout); + expect(result).toHaveProperty("notification"); + expect(result.notification).toHaveProperty("published", true); + expect(result.notification).toHaveProperty("channel", "my-channel"); + expect(result.notification).toHaveProperty("messageData", true); + }); + }); + describe("error handling", () => { it("should handle API errors", async () => { const mock = getMockAblyRest(); From 92b5eafe082fa381ae9f43a0d4ea97541b03ae6b Mon Sep 17 00:00:00 2001 From: umair Date: Fri, 10 Apr 2026 11:22:09 +0100 Subject: [PATCH 2/2] Address claude comments around hiding message data and test structure --- src/commands/push/publish.ts | 2 +- test/unit/commands/push/publish.test.ts | 28 ++++++++++++------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/commands/push/publish.ts b/src/commands/push/publish.ts index 6f31b0af..eab732dd 100644 --- a/src/commands/push/publish.ts +++ b/src/commands/push/publish.ts @@ -296,7 +296,7 @@ export default class PushPublish extends AblyBaseCommand { notification: { published: true, channel: channelName, - ...(flags.message ? { messageData: true } : {}), + ...(flags.message ? { messageData: publishMessage.data } : {}), }, }, flags, diff --git a/test/unit/commands/push/publish.test.ts b/test/unit/commands/push/publish.test.ts index 44b8b41d..885ee7b6 100644 --- a/test/unit/commands/push/publish.test.ts +++ b/test/unit/commands/push/publish.test.ts @@ -196,9 +196,7 @@ describe("push:publish command", () => { expect(result.notification).toHaveProperty("published", true); expect(result.notification).toHaveProperty("channel", "my-channel"); }); - }); - describe("--message flag (realtime message data)", () => { it("should include string message data when publishing via channel", async () => { const mock = getMockAblyRest(); const channel = mock.channels._getChannel("my-channel"); @@ -259,18 +257,6 @@ describe("push:publish command", () => { ); }); - it("should fail when --message is used without --channel", async () => { - const { error } = await runCommand( - ["push:publish", "--message", "hello", "--title", "Hi"], - import.meta.url, - ); - - expect(error).toBeDefined(); - expect(error?.message).toContain( - "--message can only be used with --channel", - ); - }); - it("should ignore --message when direct recipient overrides --channel", async () => { const mock = getMockAblyRest(); @@ -316,11 +302,23 @@ describe("push:publish command", () => { expect(result).toHaveProperty("notification"); expect(result.notification).toHaveProperty("published", true); expect(result.notification).toHaveProperty("channel", "my-channel"); - expect(result.notification).toHaveProperty("messageData", true); + expect(result.notification).toHaveProperty("messageData", "hello"); }); }); describe("error handling", () => { + it("should fail when --message is used without --channel", async () => { + const { error } = await runCommand( + ["push:publish", "--message", "hello", "--title", "Hi"], + import.meta.url, + ); + + expect(error).toBeDefined(); + expect(error?.message).toContain( + "--message can only be used with --channel", + ); + }); + it("should handle API errors", async () => { const mock = getMockAblyRest(); mock.push.admin.publish.mockRejectedValue(new Error("Publish failed"));