From 2d9dcf2d8240147ff3fdd7b94fd239cefaca9c7a Mon Sep 17 00:00:00 2001 From: Yanay Date: Thu, 30 Jul 2026 10:58:18 +0300 Subject: [PATCH] feat(blocks): node-sdk block functions + list opt-in filter Add createBlock/deleteBlock/fetchBlockStatus/fetchBlockedUsers to the users module (act-on-behalf via actingUserId), mirroring the server block endpoints exactly. fetchBlockedUsers maps actingUserId to the /blocks route's userId query param. Expose blockedFilter ("exclude" | "include-outbound-blocked") on the entity and comment list functions. Minor version bump for the new public API. Co-Authored-By: Claude Opus 4.8 --- __tests__/users.test.ts | 73 +++++++++++++++++++++++ package.json | 2 +- src/interfaces/Block.ts | 23 +++++++ src/modules/comments/fetchManyComments.ts | 5 ++ src/modules/entities/fetchManyEntities.ts | 6 ++ src/modules/users/createBlock.ts | 21 +++++++ src/modules/users/deleteBlock.ts | 18 ++++++ src/modules/users/fetchBlockStatus.ts | 21 +++++++ src/modules/users/fetchBlockedUsers.ts | 28 +++++++++ src/modules/users/index.ts | 4 ++ 10 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 src/interfaces/Block.ts create mode 100644 src/modules/users/createBlock.ts create mode 100644 src/modules/users/deleteBlock.ts create mode 100644 src/modules/users/fetchBlockStatus.ts create mode 100644 src/modules/users/fetchBlockedUsers.ts diff --git a/__tests__/users.test.ts b/__tests__/users.test.ts index 9a91cc3..1ccfcf0 100644 --- a/__tests__/users.test.ts +++ b/__tests__/users.test.ts @@ -1,7 +1,11 @@ import { checkUsernameAvailability, + createBlock, createFollow, + deleteBlock, deleteFollow, + fetchBlockStatus, + fetchBlockedUsers, deleteUser, fetchConnectionStatus, fetchConnectionsByUserId, @@ -196,6 +200,38 @@ describe("node-sdk users — graph actions — request shaping (actingUserId vs params: { actingUserId: "requester1" }, }); }); + + it("createBlock posts actingUserId in the body, userId (target) in the path", async () => { + const { client, projectInstance } = makeClient(); + await createBlock(client, { userId: "target1", actingUserId: "blocker1" }); + expect(projectInstance.post).toHaveBeenCalledWith("/users/target1/block", { + actingUserId: "blocker1", + }); + }); + + it("deleteBlock sends actingUserId as a param, userId (target) in the path", async () => { + const { client, projectInstance } = makeClient(); + await deleteBlock(client, { userId: "target1", actingUserId: "blocker1" }); + expect(projectInstance.delete).toHaveBeenCalledWith("/users/target1/block", { + params: { actingUserId: "blocker1" }, + }); + }); + + it("fetchBlockStatus sends actingUserId as a param, userId (target) in the path", async () => { + const { client, projectInstance } = makeClient(); + await fetchBlockStatus(client, { userId: "target1", actingUserId: "blocker1" }); + expect(projectInstance.get).toHaveBeenCalledWith("/users/target1/block", { + params: { actingUserId: "blocker1" }, + }); + }); + + it("fetchBlockedUsers hits /blocks with the actor as the userId query param", async () => { + const { client, projectInstance } = makeClient(); + await fetchBlockedUsers(client, { actingUserId: "blocker1", page: 2 }); + expect(projectInstance.get).toHaveBeenCalledWith("/blocks", { + params: { userId: "blocker1", page: 2 }, + }); + }); }); describe("node-sdk users — response mapping", () => { @@ -339,4 +375,41 @@ describe("node-sdk users — response mapping", () => { }), ).resolves.toEqual(result); }); + + it("createBlock returns response.data", async () => { + const { client, projectInstance } = makeClient(); + const block = { id: "blk1", blockerId: "blocker1", blockedId: "target1" }; + projectInstance.post.mockResolvedValueOnce({ data: block }); + await expect( + createBlock(client, { userId: "target1", actingUserId: "blocker1" }), + ).resolves.toEqual(block); + }); + + it("deleteBlock resolves to undefined", async () => { + const { client } = makeClient(); + await expect( + deleteBlock(client, { userId: "target1", actingUserId: "blocker1" }), + ).resolves.toBeUndefined(); + }); + + it("fetchBlockStatus returns response.data", async () => { + const { client, projectInstance } = makeClient(); + const result = { blocked: true, blockId: "blk1" }; + projectInstance.get.mockResolvedValueOnce({ data: result }); + await expect( + fetchBlockStatus(client, { userId: "target1", actingUserId: "blocker1" }), + ).resolves.toEqual(result); + }); + + it("fetchBlockedUsers returns the full PaginatedResponse envelope", async () => { + const { client, projectInstance } = makeClient(); + const envelope = { + data: [{ id: "blk1", blockedUser: { id: "target1" }, createdAt: "now" }], + pagination: { page: 1, limit: 10, total: 1 }, + }; + projectInstance.get.mockResolvedValueOnce({ data: envelope }); + await expect( + fetchBlockedUsers(client, { actingUserId: "blocker1" }), + ).resolves.toEqual(envelope); + }); }); diff --git a/package.json b/package.json index 282dc51..032cd7c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sublay/node", - "version": "7.7.3", + "version": "7.8.0", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ diff --git a/src/interfaces/Block.ts b/src/interfaces/Block.ts new file mode 100644 index 0000000..0c5b9fd --- /dev/null +++ b/src/interfaces/Block.ts @@ -0,0 +1,23 @@ +import { User } from "./User"; + +/** A directed block edge: `blockerId` has blocked `blockedId`. */ +export interface Block { + id: string; + blockerId: string; + blockedId: string; + createdAt: string; +} + +/** Outbound-only block status: does the acting user block the target? */ +export interface BlockStatusResponse { + blocked: boolean; + blockId?: string; + createdAt?: string; +} + +/** A row in the acting user's own block list, carrying the blocked user's summary. */ +export interface BlockedUser { + id: string; + blockedUser: User | null; + createdAt: string; +} diff --git a/src/modules/comments/fetchManyComments.ts b/src/modules/comments/fetchManyComments.ts index 9a3e82d..86cc3d9 100644 --- a/src/modules/comments/fetchManyComments.ts +++ b/src/modules/comments/fetchManyComments.ts @@ -26,6 +26,11 @@ export interface FetchManyCommentsProps extends SpaceReputationContextParams { sortDir?: "asc" | "desc"; include?: string; sourceId?: string; + + // Block filtering: comments authored by users the viewer is block-edged with + // are hidden by default ("exclude"). "include-outbound-blocked" re-includes + // ONLY the viewer's own outbound-blocked authors; never inbound. + blockedFilter?: "exclude" | "include-outbound-blocked"; } export async function fetchManyComments( diff --git a/src/modules/entities/fetchManyEntities.ts b/src/modules/entities/fetchManyEntities.ts index de25fa3..c3fffc6 100644 --- a/src/modules/entities/fetchManyEntities.ts +++ b/src/modules/entities/fetchManyEntities.ts @@ -96,6 +96,12 @@ export interface FetchManyEntitiesProps extends SpaceReputationContextParams { // NSFW filtering (keyed off effective NSFW) nsfwFilter?: "include-all" | "exclude" | "only"; + + // Block filtering: entities authored by users the viewer is block-edged with + // are hidden by default ("exclude"). "include-outbound-blocked" re-includes + // ONLY the viewer's own outbound-blocked authors (their "view anyway" choice); + // it never surfaces content from users who blocked the viewer. + blockedFilter?: "exclude" | "include-outbound-blocked"; } export async function fetchManyEntities( diff --git a/src/modules/users/createBlock.ts b/src/modules/users/createBlock.ts new file mode 100644 index 0000000..2bfdbe8 --- /dev/null +++ b/src/modules/users/createBlock.ts @@ -0,0 +1,21 @@ +import { SublayHttpClient } from "../../core/client"; +import { Block } from "../../interfaces/Block"; + +export interface CreateBlockProps { + /** The user being blocked (the target). */ + userId: string; + /** The user performing the block (the blocker). */ + actingUserId: string; +} + +export async function createBlock( + client: SublayHttpClient, + data: CreateBlockProps +): Promise { + const { userId, actingUserId } = data; + const response = await client.projectInstance.post( + `/users/${userId}/block`, + { actingUserId } + ); + return response.data; +} diff --git a/src/modules/users/deleteBlock.ts b/src/modules/users/deleteBlock.ts new file mode 100644 index 0000000..3912ee2 --- /dev/null +++ b/src/modules/users/deleteBlock.ts @@ -0,0 +1,18 @@ +import { SublayHttpClient } from "../../core/client"; + +export interface DeleteBlockProps { + /** The user being unblocked (the target). */ + userId: string; + /** The user performing the unblock (the blocker). */ + actingUserId: string; +} + +export async function deleteBlock( + client: SublayHttpClient, + data: DeleteBlockProps +): Promise { + const { userId, actingUserId } = data; + await client.projectInstance.delete(`/users/${userId}/block`, { + params: { actingUserId }, + }); +} diff --git a/src/modules/users/fetchBlockStatus.ts b/src/modules/users/fetchBlockStatus.ts new file mode 100644 index 0000000..21decae --- /dev/null +++ b/src/modules/users/fetchBlockStatus.ts @@ -0,0 +1,21 @@ +import { SublayHttpClient } from "../../core/client"; +import { BlockStatusResponse } from "../../interfaces/Block"; + +export interface FetchBlockStatusProps { + /** The user whose block relationship is being checked (the target). */ + userId: string; + /** The user whose perspective the status is from (the blocker). */ + actingUserId: string; +} + +export async function fetchBlockStatus( + client: SublayHttpClient, + data: FetchBlockStatusProps +): Promise { + const { userId, actingUserId } = data; + const response = await client.projectInstance.get( + `/users/${userId}/block`, + { params: { actingUserId } } + ); + return response.data; +} diff --git a/src/modules/users/fetchBlockedUsers.ts b/src/modules/users/fetchBlockedUsers.ts new file mode 100644 index 0000000..0099a98 --- /dev/null +++ b/src/modules/users/fetchBlockedUsers.ts @@ -0,0 +1,28 @@ +import { SublayHttpClient } from "../../core/client"; +import { BlockedUser } from "../../interfaces/Block"; +import { PaginatedResponse } from "../../interfaces/IPaginatedResponse"; + +export interface FetchBlockedUsersProps { + /** + * The user whose own outbound block list is being read (the blocker). The + * server resolves this via `resolveActingUserId` (service/master keys), so it + * is sent as the `userId` query param on the `/blocks` route — mirroring the + * server's act-on-behalf handling for the private block list. + */ + actingUserId: string; + page?: number; + limit?: number; +} + +export async function fetchBlockedUsers( + client: SublayHttpClient, + data: FetchBlockedUsersProps +): Promise> { + const { actingUserId, ...rest } = data; + const response = await client.projectInstance.get< + PaginatedResponse + >(`/blocks`, { + params: { userId: actingUserId, ...rest }, + }); + return response.data; +} diff --git a/src/modules/users/index.ts b/src/modules/users/index.ts index 354e7c6..1739495 100644 --- a/src/modules/users/index.ts +++ b/src/modules/users/index.ts @@ -17,3 +17,7 @@ export { fetchFollowStatus } from "./fetchFollowStatus"; export { requestConnection } from "./requestConnection"; export { fetchConnectionStatus } from "./fetchConnectionStatus"; export { removeConnectionByUserId } from "./removeConnectionByUserId"; +export { createBlock } from "./createBlock"; +export { deleteBlock } from "./deleteBlock"; +export { fetchBlockStatus } from "./fetchBlockStatus"; +export { fetchBlockedUsers } from "./fetchBlockedUsers";