From 89632ac07a896a5731fd8b73cbf495bfbb7a67a9 Mon Sep 17 00:00:00 2001 From: Yanay Date: Tue, 23 Jun 2026 09:42:58 +0300 Subject: [PATCH] feat(push): add client.push.send to the Node SDK Binds a new push module exposing send({ userIds, title, body, data? }), mirroring the server's POST /push-notifications/send contract exactly (flat body, no client-side reshaping). Adds SendPushResult/PushDeviceResult to the public interface exports and documents the new module in CLAUDE.md. --- CLAUDE.md | 11 ++++++++++- __tests__/push.test.ts | 36 ++++++++++++++++++++++++++++++++++++ src/index.ts | 4 ++++ src/interfaces/Push.ts | 9 +++++++++ src/modules/push/index.ts | 1 + src/modules/push/send.ts | 18 ++++++++++++++++++ 6 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 __tests__/push.test.ts create mode 100644 src/interfaces/Push.ts create mode 100644 src/modules/push/index.ts create mode 100644 src/modules/push/send.ts diff --git a/CLAUDE.md b/CLAUDE.md index f796d10..02c9e98 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,7 +33,7 @@ pnpm publish-prod ### Module Structure -The SDK exposes **15 modules** bound on `SublayClient` (camelCase accessor in +The SDK exposes **16 modules** bound on `SublayClient` (camelCase accessor in parentheses). Every endpoint is reached with a **service key**; operations that act on behalf of a user take an explicit `userId` (or `actingUserId` on the nested `users` follow/connection routes and the `chat` target routes), since a @@ -60,6 +60,7 @@ src/ │ ├── reports/ # client.reports (service-key userId) │ ├── app-notifications/ # client.appNotifications (service-key userId) │ ├── storage/ # client.storage (service-key userId) +│ ├── push/ # client.push (service-key userIds batch) │ └── chat/ # client.chat (service-key userId / actingUserId) └── index.ts # Main entry point with SublayClient class ``` @@ -229,6 +230,14 @@ Reactions: `toggleReaction`, `listReactions` · Read state: `markAsRead` are service-key god-mode (space-moderator check bypassed); their `actingUserId` is attribution-only. See `plan-chat-node-sdk-parity.md` at the engine root. +### 16. Push Notifications Module (1 function) + +`send` — `{ userIds: string[], title: string, body: string, data?: Record }`. +Fans a push out across all of the listed users' registered devices (APNs/FCM/Web +Push); returns per-user, per-device results (`{ results: { [userId]: { platform, success, reason? }[] } }`). +Devices for a user with no registered devices come back as an empty array, not +omitted. Capped at 100 `userIds` per call. + ## Key Design Patterns ### 1. Foreign ID Integration diff --git a/__tests__/push.test.ts b/__tests__/push.test.ts new file mode 100644 index 0000000..8e0afbf --- /dev/null +++ b/__tests__/push.test.ts @@ -0,0 +1,36 @@ +import { send } from "../src/modules/push"; +import { makeClient } from "./helpers/mockClient"; + +describe("node-sdk push — request shaping", () => { + it("send posts the full body to /push-notifications/send with no reshaping", async () => { + const { client, projectInstance } = makeClient(); + await send(client, { + userIds: ["u1", "u2"], + title: "New message", + body: "You have a new message", + data: { conversationId: "c1" }, + }); + + expect(projectInstance.post).toHaveBeenCalledWith("/push-notifications/send", { + userIds: ["u1", "u2"], + title: "New message", + body: "You have a new message", + data: { conversationId: "c1" }, + }); + }); + + it("send returns the typed response passed through as-is", async () => { + const { client, projectInstance } = makeClient(); + const responseBody = { + results: { + u1: [{ platform: "ios", success: true }], + u2: [], + }, + }; + projectInstance.post.mockResolvedValueOnce({ data: responseBody }); + + const result = await send(client, { userIds: ["u1", "u2"], title: "Hi", body: "There" }); + + expect(result).toEqual(responseBody); + }); +}); diff --git a/src/index.ts b/src/index.ts index 135bd8a..42a680c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import * as Entities from "./modules/entities"; import * as Events from "./modules/events"; import * as Follows from "./modules/follows"; import * as HostedApps from "./modules/hosted-apps"; +import * as Push from "./modules/push"; import * as Reports from "./modules/reports"; import * as Search from "./modules/search"; import * as Spaces from "./modules/spaces"; @@ -39,6 +40,7 @@ export class SublayClient { public events: BoundModule; public follows: BoundModule; public hostedApps: BoundModule; + public push: BoundModule; public reports: BoundModule; public search: BoundModule; public spaces: BoundModule; @@ -59,6 +61,7 @@ export class SublayClient { this.events = bindModule(Events, this.http); this.follows = bindModule(Follows, this.http); this.hostedApps = bindModule(HostedApps, this.http); + this.push = bindModule(Push, this.http); this.reports = bindModule(Reports, this.http); this.search = bindModule(Search, this.http); this.spaces = bindModule(Spaces, this.http); @@ -150,6 +153,7 @@ export type { ChatMessage } from "./interfaces/ChatMessage"; export type { Reaction, ReactionType, ReactionCounts } from "./interfaces/Reaction"; export type { UnifiedAppNotification, PotentiallyPopulatedUnifiedAppNotification } from "./interfaces/AppNotification"; export type { OAuthIdentity } from "./interfaces/OAuthIdentity"; +export type { PushDeviceResult, SendPushResult } from "./interfaces/Push"; export type { Report, CreateReportResponse } from "./interfaces/Report"; export type { File, FileImage, FileImageVariant } from "./interfaces/File"; export type { diff --git a/src/interfaces/Push.ts b/src/interfaces/Push.ts new file mode 100644 index 0000000..e092a1c --- /dev/null +++ b/src/interfaces/Push.ts @@ -0,0 +1,9 @@ +export interface PushDeviceResult { + platform: string; + success: boolean; + reason?: string; +} + +export interface SendPushResult { + results: Record; +} diff --git a/src/modules/push/index.ts b/src/modules/push/index.ts new file mode 100644 index 0000000..10be63c --- /dev/null +++ b/src/modules/push/index.ts @@ -0,0 +1 @@ +export { send } from "./send"; diff --git a/src/modules/push/send.ts b/src/modules/push/send.ts new file mode 100644 index 0000000..ca355b4 --- /dev/null +++ b/src/modules/push/send.ts @@ -0,0 +1,18 @@ +import { SublayHttpClient } from "../../core/client"; +import { SendPushResult } from "../../interfaces/Push"; + +export interface SendPushProps { + userIds: string[]; + title: string; + body: string; + data?: Record; +} + +export async function send( + client: SublayHttpClient, + data: SendPushProps +): Promise { + const path = `/push-notifications/send`; + const response = await client.projectInstance.post(path, data); + return response.data; +}