Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
16b721f
test: add shared mock-client test helper
Tsabary Jun 22, 2026
c678ed3
test: cover SublayHttpClient construction and verifyClient
Tsabary Jun 22, 2026
8903edd
ci: add GitHub Actions test workflow and gate publish on it
Tsabary Jun 22, 2026
942b4dd
test: add response-mapping coverage for tables and tables-management
Tsabary Jun 22, 2026
78a0ea2
test: cover the entities module (request shaping + response mapping)
Tsabary Jun 22, 2026
e48084e
test: cover the comments module (request shaping + response mapping)
Tsabary Jun 22, 2026
dc895b4
test: cover the users module (request shaping + response mapping)
Tsabary Jun 22, 2026
03b25ef
test: cover the collections module (request shaping + response mapping)
Tsabary Jun 22, 2026
80f1fc5
test: cover the follows module (request shaping + response mapping)
Tsabary Jun 22, 2026
798e94c
test: cover the connections module (request shaping + response mapping)
Tsabary Jun 22, 2026
849f813
test: cover the spaces module lifecycle subset
Tsabary Jun 22, 2026
51168f7
test: cover the spaces module membership subset
Tsabary Jun 22, 2026
32dc16b
test: cover the spaces module moderation, rules & digest subset
Tsabary Jun 22, 2026
d46f7cd
test: cover the search module (request shaping + response mapping)
Tsabary Jun 22, 2026
fda3058
test: cover the appNotifications module (request shaping + response m…
Tsabary Jun 22, 2026
f9868cd
test: cover the auth module (request shaping + response mapping)
Tsabary Jun 22, 2026
cab0bbe
test: cover the hostedApps and reports modules
Tsabary Jun 22, 2026
c41a4fc
test: cover the storage module (request shaping + response mapping)
Tsabary Jun 22, 2026
f13869c
test: cover the chat module (request shaping + response mapping)
Tsabary Jun 22, 2026
1a0b6f6
test: cover the events module; fix CLAUDE.md module-binding staleness
Tsabary Jun 22, 2026
a1981b7
ci: bump pnpm to v11 to fix CI failure on pnpm-workspace.yaml
Tsabary Jun 22, 2026
df88a01
ci: pin pnpm to v10 instead of v11 (Node 20 incompatible with v11)
Tsabary Jun 22, 2026
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
28 changes: 28 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Test

on:
pull_request:
push:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 10

- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run test suite
run: pnpm test
42 changes: 25 additions & 17 deletions 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 **14 modules** bound on `SublayClient` (camelCase accessor in
The SDK exposes **15 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 @@ -47,6 +47,7 @@ src/
│ # Collection, Connection, Follow, Report, Space, …)
├── modules/
│ ├── entities/ # client.entities
│ ├── events/ # client.events
│ ├── comments/ # client.comments
│ ├── users/ # client.users (incl. nested follow/connection actions)
│ ├── spaces/ # client.spaces
Expand All @@ -65,8 +66,7 @@ src/

> **Not bound (unmounted):** `oauth` only. It's a browser redirect flow with no
> meaningful server-to-server contract — the directory stays on disk but is not
> exposed on `SublayClient`. (`chat` and `storage` were previously deferred here;
> both are now bound — see §13 and §14 below and `plan-chat-node-sdk-parity.md`.)
> exposed on `SublayClient`.
>
> The space-scoped chat endpoints (`getSpaceConversation`, `moderateSpaceChatMessage`,
> `handleSpaceChatReport`) live on **`client.spaces`** (they're `/spaces/...` routes),
Expand Down Expand Up @@ -101,7 +101,7 @@ const client = await SublayClient.init({

## API Modules & Features

`SublayClient` binds **14 modules**. The source of truth is `src/index.ts` (the `bindModule` calls) and each module's `index.ts`. The full public surface and per-function props/returns are documented in `docs/v7/node-sdk/`.
`SublayClient` binds **15 modules**. The source of truth is `src/index.ts` (the `bindModule` calls) and each module's `index.ts`. The full public surface and per-function props/returns are documented in `docs/v7/node-sdk/`.

**Acting on behalf of a user**: the SDK authenticates as the project (service key), not as an end user. So user-scoped functions take an explicit `userId` — the user the operation is performed as. A few routes act on one user *toward another* and take the actor as `actingUserId` while `userId` is the target: the nested follow/connection routes on the `users` module, and `chat.createDirectConversation` / `chat.addMember` / `chat.removeMember` / `chat.changeMemberRole`.

Expand All @@ -115,13 +115,21 @@ Core content objects (posts, articles, products, listings, etc.).

`fetchManyEntities` supports rich filtering via object-shaped filters: `keywordsFilters`, `metadataFilters` (`includes`/`includesAny`/`doesNotInclude`/`exists`/`doesNotExist`), `titleFilters`, `contentFilters`, `attachmentsFilters`, `locationFilters`; plus `sortBy` (new/hot/top/controversial or `metadata.<prop>`), `sortDir`, `sortType`, `sortByReaction`, `timeFrame`, `userId`/`followedOnly`, `include`, pagination.

### 2. Comments Module (10 functions)
### 2. Events Module (14 functions)

Event lifecycle, RSVPs, hosts, and invites.

`createEvent`, `fetchEvent`, `fetchManyEvents`, `updateEvent`, `cancelEvent`, `deleteEvent`, `setRsvp`, `withdrawRsvp`, `addHost`, `removeHost`, `addInvite`, `removeInvite`, `fetchInvitees`, `fetchEventRsvps`

`EventType` is `online`/`physical`/`hybrid`; `EventVisibility` is `public`/`members`/`invite`; `RsvpStatus` is `going`/`maybe`/`not_going`. `fetchManyEvents` filters: `timeWindow` (upcoming/ongoing/past) or an explicit `startsAfter`/`startsBefore` range, `spaceId`, `hostId`, `type`, `status` (defaults to `active`), `myRsvp` (requires `userId`), `locationFilters`, `titleFilters`/`descriptionFilters`. `fetchInvitees` (the guest list) is host-only; `fetchEventRsvps` is visible to hosts, to any viewer when `guestListVisible` is true, or to service/master keys.

### 3. Comments Module (10 functions)

`createComment`, `fetchComment`, `fetchCommentByForeignId`, `updateComment`, `deleteComment`, `fetchManyComments`, `addReaction`, `removeReaction`, `fetchReactions`, `getUserReaction`

Threaded replies (`parentId`), quote-replies (`referencedCommentId`), GIFs, mentions, reactions, foreign IDs, soft deletes. `fetchManyComments` sorts by `sortBy` (new/old/top/controversial).

### 3. Users Module (18 functions)
### 4. Users Module (18 functions)

Read/update profiles and query + act on the follow/connection graph.

Expand All @@ -131,25 +139,25 @@ Graph queries (by target user ID): `fetchFollowersByUserId`, `fetchFollowersCoun

Graph actions (nested routes, take `actingUserId` + path `userId`): `createFollow`, `deleteFollow`, `fetchFollowStatus`, `requestConnection`, `fetchConnectionStatus`, `removeConnectionByUserId`

### 4. Collections Module (8 functions)
### 5. Collections Module (8 functions)

A user's saved-content collections (replaces the old "lists"). All take `userId`.

`fetchRootCollection`, `fetchSubCollections`, `createNewCollection`, `fetchCollectionEntities`, `addEntityToCollection`, `removeEntityFromCollection`, `updateCollection`, `deleteCollection`

### 5. Follows Module (5 functions)
### 6. Follows Module (5 functions)

Read a user's follow graph and remove follows by record ID. All take `userId`.

`fetchFollowing`, `fetchFollowers`, `fetchFollowingCount`, `fetchFollowersCount`, `deleteFollow`

### 6. Connections Module (7 functions)
### 7. Connections Module (7 functions)

A user's mutual connections and pending requests. All take `userId`.

`fetchConnections`, `fetchConnectionsCount`, `fetchSentPendingConnections`, `fetchReceivedPendingConnections`, `acceptConnection`, `declineConnection`, `removeConnection`

### 7. Spaces Module (33 functions)
### 8. Spaces Module (33 functions)

Space lifecycle, membership, moderation, rules, and digest config — documented across three pages (`spaces`, `spaces-members`, `spaces-moderation`).

Expand All @@ -159,27 +167,27 @@ Members: `joinSpace`, `leaveSpace`, `checkMyMembership`, `fetchSpaceMembers`, `f

Moderation/rules/digest: `handleEntityReport`, `handleCommentReport` (both take an `actions` array), `moderateSpaceEntity`, `moderateSpaceComment`, `fetchManyRules`, `fetchRule`, `createRule`, `updateRule`, `deleteRule`, `reorderRules`, `fetchDigestConfig`, `updateDigestConfig`

### 8. Search Module (4 functions)
### 9. Search Module (4 functions)

`searchContent`, `searchUsers`, `searchSpaces`, `askContent` (AI Q&A). Content/ask take `query`, `sourceTypes`, `spaceId`, `conversationId`, `limit`.

### 9. Reports Module (2 functions)
### 10. Reports Module (2 functions)

`createReport` (reporter is `userId`), `fetchModeratedReports` (moderator is `userId`). `ReportStatus`: `pending` | `on-hold` | `escalated` | `dismissed` | `actioned`.

### 10. App Notifications Module (4 functions)
### 11. App Notifications Module (4 functions)

`fetchNotifications`, `countUnreadNotifications`, `markNotificationAsRead`, `markAllNotificationsAsRead`. All take `userId`.

### 11. Auth Module (10 functions)
### 12. Auth Module (10 functions)

`signUp`, `signIn`, `signOut`, `requestNewAccessToken`, `verifyExternalUser`, `requestPasswordReset`, `resetPassword`, `verifyEmail`, `sendVerificationEmail`, `changePassword`

### 12. Hosted Apps Module (1 function)
### 13. Hosted Apps Module (1 function)

`fetchHostedApp` — fetches hosted app configuration (uses the internal axios instance).

### 13. Storage Module (4 functions)
### 14. Storage Module (4 functions)

File and image uploads, plus read/delete. `uploadFile`, `uploadImage`, `getFile`, `deleteFile`.

Expand All @@ -195,7 +203,7 @@ File and image uploads, plus read/delete. `uploadFile`, `uploadImage`, `getFile`
owner-or-service** — a user token may only delete files it owns, service/master keys may delete any.
Both enforced server-side in `server/src/{v7,v7-schema}/controllers/storage/`.

### 14. Chat Module (20 functions)
### 15. Chat Module (20 functions)

Conversations, messages, members, reactions, read state, and reporting.

Expand Down
78 changes: 78 additions & 0 deletions __tests__/app-notifications.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {
countUnreadNotifications,
fetchNotifications,
markAllNotificationsAsRead,
markNotificationAsRead,
} from "../src/modules/app-notifications";
import { makeClient } from "./helpers/mockClient";

describe("node-sdk appNotifications — request shaping", () => {
it("fetchNotifications hits /app-notifications with the full body as params", async () => {
const { client, projectInstance } = makeClient();
await fetchNotifications(client, { userId: "u1", page: 1 });
expect(projectInstance.get).toHaveBeenCalledWith("/app-notifications", {
params: { userId: "u1", page: 1 },
});
});

it("countUnreadNotifications hits /app-notifications/count with the full body as params", async () => {
const { client, projectInstance } = makeClient();
await countUnreadNotifications(client, { userId: "u1" });
expect(projectInstance.get).toHaveBeenCalledWith("/app-notifications/count", {
params: { userId: "u1" },
});
});

it("markNotificationAsRead patches the mark-as-read route with userId in the body", async () => {
const { client, projectInstance } = makeClient();
await markNotificationAsRead(client, { notificationId: "n1", userId: "u1" });
expect(projectInstance.patch).toHaveBeenCalledWith(
"/app-notifications/n1/mark-as-read",
{ userId: "u1" },
);
});

it("markAllNotificationsAsRead patches the mark-all-as-read route with userId in the body", async () => {
const { client, projectInstance } = makeClient();
await markAllNotificationsAsRead(client, { userId: "u1" });
expect(projectInstance.patch).toHaveBeenCalledWith(
"/app-notifications/mark-all-as-read",
{ userId: "u1" },
);
});
});

describe("node-sdk appNotifications — response mapping", () => {
it("fetchNotifications returns the full PaginatedResponse envelope", async () => {
const { client, projectInstance } = makeClient();
const envelope = { data: [{ id: "n1" }], pagination: { page: 1, limit: 10, total: 1 } };
projectInstance.get.mockResolvedValueOnce({ data: envelope });
await expect(fetchNotifications(client, { userId: "u1" })).resolves.toEqual(
envelope,
);
});

it("countUnreadNotifications returns response.data as a raw number", async () => {
const { client, projectInstance } = makeClient();
projectInstance.get.mockResolvedValueOnce({ data: 3 });
await expect(
countUnreadNotifications(client, { userId: "u1" }),
).resolves.toBe(3);
});

it("markNotificationAsRead resolves to undefined", async () => {
const { client } = makeClient();
await expect(
markNotificationAsRead(client, { notificationId: "n1", userId: "u1" }),
).resolves.toBeUndefined();
});

it("markAllNotificationsAsRead returns response.data", async () => {
const { client, projectInstance } = makeClient();
const result = { markedAsRead: 5 };
projectInstance.patch.mockResolvedValueOnce({ data: result });
await expect(
markAllNotificationsAsRead(client, { userId: "u1" }),
).resolves.toEqual(result);
});
});
Loading
Loading