Skip to content

Commit 75e54ca

Browse files
committed
Fix SDK drift against kan.bn API: paths, mandatory fields, renamed inputs
1 parent adfe907 commit 75e54ca

15 files changed

Lines changed: 367 additions & 449 deletions

FUNCTIONS.md

Lines changed: 120 additions & 99 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@centrolabs/kan-sdk",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"description": "Unofficial SDK for kan.bn API",
55
"author": "centrolabs",
66
"homepage": "https://github.com/centrolabs/kan-sdk",

src/client.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,12 @@ describe("KanClient", () => {
185185
test("JSON-stringifies PATCH body", async () => {
186186
const mock = createMockFetch();
187187
const restore = withMock(mock);
188-
mock.registerPatch("/workspaces/ws_1", { publicId: "ws_1", name: "Updated", slug: "ws", createdAt: "", updatedAt: "" });
188+
mock.registerPatch("/checklists/items/item_1", { publicId: "item_1", checklistPublicId: "chk_1", title: "Done", completed: true, index: 0, createdAt: "", updatedAt: "" });
189189

190190
const kan = createKan({ apiKey: "kan_test" });
191-
await kan.workspaces.update("ws_1", { name: "Updated", description: "New desc" });
191+
await kan.cards.updateChecklistItem("item_1", { title: "Done", completed: true });
192192

193-
expect(mock.calls[0].body).toEqual({ name: "Updated", description: "New desc" });
193+
expect(mock.calls[0].body).toEqual({ title: "Done", completed: true });
194194
restore();
195195
});
196196

src/concerns/boards.test.ts

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,39 @@ describe("BoardsConcern", () => {
2424
restore();
2525
});
2626

27-
test("getBySlug calls /boards/slug/:slug", async () => {
27+
test("getBySlug calls /workspaces/:workspaceSlug/boards/:boardSlug", async () => {
2828
const mock = createMockFetch();
2929
const restore = withMock(mock);
30-
mock.registerGet("/boards/slug/my-board", { publicId: "brd_1", name: "Board", slug: "my-board", workspacePublicId: "ws_1", type: "regular", createdAt: "", updatedAt: "" });
30+
mock.registerGet("/workspaces/my-ws/boards/my-board", { publicId: "brd_1", name: "Board", slug: "my-board", workspacePublicId: "ws_1", type: "regular", createdAt: "", updatedAt: "" });
3131

3232
const kan = createKan({ apiKey: "kan_test" });
33-
await kan.boards.getBySlug("my-board");
33+
await kan.boards.getBySlug({ workspaceSlug: "my-ws", boardSlug: "my-board" });
3434

35-
expect(mock.calls[0].url).toContain("/boards/slug/my-board");
35+
expect(mock.calls[0].url).toContain("/workspaces/my-ws/boards/my-board");
3636
restore();
3737
});
3838

39-
test("create posts correct body", async () => {
39+
test("create posts to /workspaces/:id/boards with mandatory lists/labels", async () => {
4040
const mock = createMockFetch();
4141
const restore = withMock(mock);
42-
mock.registerPost("/boards", { publicId: "brd_new", name: "New Board", slug: "new-board", workspacePublicId: "ws_1", type: "regular", createdAt: "", updatedAt: "" });
42+
mock.registerPost("/workspaces/ws_1/boards", { publicId: "brd_new", name: "New Board", slug: "new-board", workspacePublicId: "ws_1", type: "regular", createdAt: "", updatedAt: "" });
4343

4444
const kan = createKan({ apiKey: "kan_test" });
45-
await kan.boards.create({ name: "New Board", workspacePublicId: "ws_1", type: "template", description: "A board" });
46-
47-
expect(mock.calls[0].body).toEqual({ name: "New Board", workspacePublicId: "ws_1", type: "template", description: "A board" });
45+
await kan.boards.create({
46+
name: "New Board",
47+
workspacePublicId: "ws_1",
48+
lists: ["To Do", "Done"],
49+
labels: ["urgent"],
50+
type: "template",
51+
});
52+
53+
expect(mock.calls[0].url).toContain("/workspaces/ws_1/boards");
54+
expect(mock.calls[0].body).toEqual({
55+
name: "New Board",
56+
lists: ["To Do", "Done"],
57+
labels: ["urgent"],
58+
type: "template",
59+
});
4860
restore();
4961
});
5062

@@ -54,9 +66,9 @@ describe("BoardsConcern", () => {
5466
mock.registerPut("/boards/brd_1", { publicId: "brd_1", name: "Renamed", slug: "board", workspacePublicId: "ws_1", type: "regular", createdAt: "", updatedAt: "" });
5567

5668
const kan = createKan({ apiKey: "kan_test" });
57-
await kan.boards.update("brd_1", { name: "Renamed", description: "New desc" });
69+
await kan.boards.update("brd_1", { name: "Renamed", visibility: "private", favorite: true });
5870

59-
expect(mock.calls[0].body).toEqual({ name: "Renamed", description: "New desc" });
71+
expect(mock.calls[0].body).toEqual({ name: "Renamed", visibility: "private", favorite: true });
6072
expect(mock.calls[0].method).toBe("PUT");
6173
restore();
6274
});
@@ -74,15 +86,17 @@ describe("BoardsConcern", () => {
7486
restore();
7587
});
7688

77-
test("checkSlugAvailable calls /boards/:slug/available", async () => {
89+
test("checkSlugAvailable calls /boards/:id/check-slug-availability with boardSlug", async () => {
7890
const mock = createMockFetch();
7991
const restore = withMock(mock);
80-
mock.registerGet("/boards/my-slug/available", { available: false });
92+
mock.registerGet("/boards/brd_1/check-slug-availability", { isReserved: false });
8193

8294
const kan = createKan({ apiKey: "kan_test" });
83-
const result = await kan.boards.checkSlugAvailable("my-slug");
95+
const result = await kan.boards.checkSlugAvailable("brd_1", "my-slug");
8496

85-
expect(result).toEqual({ available: false });
97+
expect(mock.calls[0].url).toContain("/boards/brd_1/check-slug-availability");
98+
expect(mock.calls[0].url).toContain("boardSlug=my-slug");
99+
expect(result).toEqual({ isReserved: false });
86100
restore();
87101
});
88102
});

src/concerns/boards.ts

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,64 +4,61 @@ import type { KanClient } from "../client";
44
export interface CreateBoardInput {
55
name: string;
66
workspacePublicId: string;
7-
description?: string;
7+
lists: string[];
8+
labels: string[];
89
type?: "regular" | "template";
10+
sourceBoardPublicId?: string;
911
}
1012

1113
export interface UpdateBoardInput {
1214
name?: string;
13-
description?: string;
15+
slug?: string;
16+
visibility?: "public" | "private";
17+
favorite?: boolean;
18+
isArchived?: boolean;
19+
}
20+
21+
export interface GetBoardBySlugInput {
22+
workspaceSlug: string;
23+
boardSlug: string;
1424
}
1525

1626
export class BoardsConcern {
1727
constructor(private client: KanClient) {}
1828

19-
/**
20-
* Retrieves a board by its public ID.
21-
* @param boardPublicId - The public ID of the board
22-
*/
2329
async getByPublicId(boardPublicId: string): Promise<Board> {
2430
return this.client.get<Board>(`/boards/${boardPublicId}`);
2531
}
2632

27-
/**
28-
* Retrieves a board by its slug.
29-
* @param slug - The slug of the board
30-
*/
31-
async getBySlug(slug: string): Promise<Board> {
32-
return this.client.get<Board>(`/boards/slug/${slug}`);
33+
async getBySlug(input: GetBoardBySlugInput): Promise<Board> {
34+
return this.client.get<Board>(
35+
`/workspaces/${input.workspaceSlug}/boards/${input.boardSlug}`
36+
);
3337
}
3438

35-
/**
36-
* Creates a new board.
37-
* @param input - The board creation input
38-
*/
3939
async create(input: CreateBoardInput): Promise<Board> {
40-
return this.client.post<Board>("/boards", input);
40+
const { workspacePublicId, ...body } = input;
41+
return this.client.post<Board>(
42+
`/workspaces/${workspacePublicId}/boards`,
43+
body
44+
);
4145
}
4246

43-
/**
44-
* Updates an existing board.
45-
* @param boardPublicId - The public ID of the board to update
46-
* @param input - The fields to update
47-
*/
4847
async update(boardPublicId: string, input: UpdateBoardInput): Promise<Board> {
4948
return this.client.put<Board>(`/boards/${boardPublicId}`, input);
5049
}
5150

52-
/**
53-
* Deletes a board.
54-
* @param boardPublicId - The public ID of the board to delete
55-
*/
5651
async delete(boardPublicId: string): Promise<void> {
5752
await this.client.delete<void>(`/boards/${boardPublicId}`);
5853
}
5954

60-
/**
61-
* Checks whether a board slug is available.
62-
* @param slug - The slug to check
63-
*/
64-
async checkSlugAvailable(slug: string): Promise<{ available: boolean }> {
65-
return this.client.get<{ available: boolean }>(`/boards/${slug}/available`);
55+
async checkSlugAvailable(
56+
boardPublicId: string,
57+
boardSlug: string
58+
): Promise<{ isReserved: boolean }> {
59+
return this.client.get<{ isReserved: boolean }>(
60+
`/boards/${boardPublicId}/check-slug-availability`,
61+
{ boardSlug }
62+
);
6663
}
6764
}

src/concerns/cards.test.ts

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,12 @@ describe("CardsConcern", () => {
6060
test("update puts with partial body", async () => {
6161
const mock = createMockFetch();
6262
const restore = withMock(mock);
63-
mock.registerPut("/cards/card_1", { publicId: "card_1", title: "Updated", listPublicId: "lst_1", boardPublicId: "brd_1", position: 1, createdAt: "", updatedAt: "" });
63+
mock.registerPut("/cards/card_1", { publicId: "card_1", title: "Updated", listPublicId: "lst_1", boardPublicId: "brd_1", index: 1, createdAt: "", updatedAt: "" });
6464

6565
const kan = createKan({ apiKey: "kan_test" });
66-
await kan.cards.update("card_1", { title: "Updated", position: 1 });
66+
await kan.cards.update("card_1", { title: "Updated", index: 1 });
6767

68-
expect(mock.calls[0].body).toEqual({ title: "Updated", position: 1 });
68+
expect(mock.calls[0].body).toEqual({ title: "Updated", index: 1 });
6969
expect(mock.calls[0].method).toBe("PUT");
7070
restore();
7171
});
@@ -155,28 +155,67 @@ describe("CardsConcern", () => {
155155
restore();
156156
});
157157

158-
test("updateChecklistItem patches nested item path", async () => {
158+
test("updateChecklist puts /checklists/:id", async () => {
159159
const mock = createMockFetch();
160160
const restore = withMock(mock);
161-
mock.registerPatch("/cards/card_1/checklists/chk_1/items/item_1", { publicId: "item_1", checklistPublicId: "chk_1", text: "Done", isChecked: true, position: 0, createdAt: "", updatedAt: "" });
161+
mock.registerPut("/checklists/chk_1", { publicId: "chk_1", cardPublicId: "card_1", name: "Renamed", createdAt: "", updatedAt: "" });
162162

163163
const kan = createKan({ apiKey: "kan_test" });
164-
await kan.cards.updateChecklistItem("card_1", "chk_1", "item_1", { text: "Done", isChecked: true });
164+
await kan.cards.updateChecklist("chk_1", { name: "Renamed" });
165165

166-
expect(mock.calls[0].url).toContain("/cards/card_1/checklists/chk_1/items/item_1");
167-
expect(mock.calls[0].body).toEqual({ text: "Done", isChecked: true });
166+
expect(mock.calls[0].url).toContain("/checklists/chk_1");
167+
expect(mock.calls[0].body).toEqual({ name: "Renamed" });
168168
restore();
169169
});
170170

171-
test("deleteChecklistItem calls DELETE on nested item path", async () => {
171+
test("deleteChecklist calls DELETE /checklists/:id", async () => {
172172
const mock = createMockFetch();
173173
const restore = withMock(mock);
174-
mock.registerDelete("/cards/card_1/checklists/chk_1/items/item_1", undefined, 204);
174+
mock.registerDelete("/checklists/chk_1", undefined, 204);
175175

176176
const kan = createKan({ apiKey: "kan_test" });
177-
await expect(kan.cards.deleteChecklistItem("card_1", "chk_1", "item_1")).resolves.toBeUndefined();
177+
await expect(kan.cards.deleteChecklist("chk_1")).resolves.toBeUndefined();
178178

179-
expect(mock.calls[0].url).toContain("/cards/card_1/checklists/chk_1/items/item_1");
179+
expect(mock.calls[0].url).toContain("/checklists/chk_1");
180+
expect(mock.calls[0].method).toBe("DELETE");
181+
restore();
182+
});
183+
184+
test("addChecklistItem posts to /checklists/:id/items", async () => {
185+
const mock = createMockFetch();
186+
const restore = withMock(mock);
187+
mock.registerPost("/checklists/chk_1/items", { publicId: "item_1", checklistPublicId: "chk_1", title: "Do", completed: false, index: 0, createdAt: "", updatedAt: "" });
188+
189+
const kan = createKan({ apiKey: "kan_test" });
190+
await kan.cards.addChecklistItem("chk_1", { title: "Do" });
191+
192+
expect(mock.calls[0].url).toContain("/checklists/chk_1/items");
193+
expect(mock.calls[0].body).toEqual({ title: "Do" });
194+
restore();
195+
});
196+
197+
test("updateChecklistItem patches /checklists/items/:id", async () => {
198+
const mock = createMockFetch();
199+
const restore = withMock(mock);
200+
mock.registerPatch("/checklists/items/item_1", { publicId: "item_1", checklistPublicId: "chk_1", title: "Done", completed: true, index: 0, createdAt: "", updatedAt: "" });
201+
202+
const kan = createKan({ apiKey: "kan_test" });
203+
await kan.cards.updateChecklistItem("item_1", { title: "Done", completed: true });
204+
205+
expect(mock.calls[0].url).toContain("/checklists/items/item_1");
206+
expect(mock.calls[0].body).toEqual({ title: "Done", completed: true });
207+
restore();
208+
});
209+
210+
test("deleteChecklistItem calls DELETE /checklists/items/:id", async () => {
211+
const mock = createMockFetch();
212+
const restore = withMock(mock);
213+
mock.registerDelete("/checklists/items/item_1", undefined, 204);
214+
215+
const kan = createKan({ apiKey: "kan_test" });
216+
await expect(kan.cards.deleteChecklistItem("item_1")).resolves.toBeUndefined();
217+
218+
expect(mock.calls[0].url).toContain("/checklists/items/item_1");
180219
expect(mock.calls[0].method).toBe("DELETE");
181220
restore();
182221
});
@@ -257,38 +296,40 @@ describe("CardsConcern", () => {
257296
restore();
258297
});
259298

260-
test("confirmAttachment posts key, filename, contentType, size", async () => {
299+
test("confirmAttachment posts s3Key, filenames, contentType, size", async () => {
261300
const mock = createMockFetch();
262301
const restore = withMock(mock);
263302
mock.registerPost("/cards/card_1/attachments/confirm", {});
264303

265304
const kan = createKan({ apiKey: "kan_test" });
266305
await kan.cards.confirmAttachment("card_1", {
267-
key: "uploads/file.pdf",
306+
s3Key: "ws_1/card_1/abc-file.pdf",
268307
filename: "file.pdf",
308+
originalFilename: "original-file.pdf",
269309
contentType: "application/pdf",
270310
size: 1024,
271311
});
272312

273313
expect(mock.calls[0].url).toContain("/cards/card_1/attachments/confirm");
274314
expect(mock.calls[0].body).toEqual({
275-
key: "uploads/file.pdf",
315+
s3Key: "ws_1/card_1/abc-file.pdf",
276316
filename: "file.pdf",
317+
originalFilename: "original-file.pdf",
277318
contentType: "application/pdf",
278319
size: 1024,
279320
});
280321
restore();
281322
});
282323

283-
test("deleteAttachment calls DELETE /cards/:id/attachments/:attId", async () => {
324+
test("deleteAttachment calls DELETE /attachments/:attId", async () => {
284325
const mock = createMockFetch();
285326
const restore = withMock(mock);
286-
mock.registerDelete("/cards/card_1/attachments/att_1", undefined, 204);
327+
mock.registerDelete("/attachments/att_1", undefined, 204);
287328

288329
const kan = createKan({ apiKey: "kan_test" });
289-
await expect(kan.cards.deleteAttachment("card_1", "att_1")).resolves.toBeUndefined();
330+
await expect(kan.cards.deleteAttachment("att_1")).resolves.toBeUndefined();
290331

291-
expect(mock.calls[0].url).toContain("/cards/card_1/attachments/att_1");
332+
expect(mock.calls[0].url).toContain("/attachments/att_1");
292333
expect(mock.calls[0].method).toBe("DELETE");
293334
restore();
294335
});

0 commit comments

Comments
 (0)