Skip to content
Merged
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
11 changes: 10 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
```
Expand Down Expand Up @@ -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<string, any> }`.
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
Expand Down
36 changes: 36 additions & 0 deletions __tests__/push.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -39,6 +40,7 @@ export class SublayClient {
public events: BoundModule<typeof Events>;
public follows: BoundModule<typeof Follows>;
public hostedApps: BoundModule<typeof HostedApps>;
public push: BoundModule<typeof Push>;
public reports: BoundModule<typeof Reports>;
public search: BoundModule<typeof Search>;
public spaces: BoundModule<typeof Spaces>;
Expand All @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions src/interfaces/Push.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface PushDeviceResult {
platform: string;
success: boolean;
reason?: string;
}

export interface SendPushResult {
results: Record<string, PushDeviceResult[]>;
}
1 change: 1 addition & 0 deletions src/modules/push/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { send } from "./send";
18 changes: 18 additions & 0 deletions src/modules/push/send.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>;
}

export async function send(
client: SublayHttpClient,
data: SendPushProps
): Promise<SendPushResult> {
const path = `/push-notifications/send`;
const response = await client.projectInstance.post<SendPushResult>(path, data);
return response.data;
}
Loading