Skip to content

Commit 8b309f0

Browse files
authored
URL encode Catalog name (#10)
1 parent 387737f commit 8b309f0

File tree

3 files changed

+182
-21
lines changed

3 files changed

+182
-21
lines changed

src/client/catalogs.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ import type { BaseIterableClient } from "./base.js";
2929
export function Catalogs<T extends Constructor<BaseIterableClient>>(Base: T) {
3030
return class extends Base {
3131
async createCatalog(catalogName: string): Promise<IterableSuccessResponse> {
32-
const response = await this.client.post(`/api/catalogs/${catalogName}`);
32+
const response = await this.client.post(
33+
`/api/catalogs/${encodeURIComponent(catalogName)}`
34+
);
3335
return this.validateResponse(response, IterableSuccessResponseSchema);
3436
}
3537

@@ -51,7 +53,7 @@ export function Catalogs<T extends Constructor<BaseIterableClient>>(Base: T) {
5153
});
5254

5355
const response = await this.client.post(
54-
`/api/catalogs/${options.catalogName}/items`,
56+
`/api/catalogs/${encodeURIComponent(options.catalogName)}/items`,
5557
{
5658
documents,
5759
replaceUploadedFieldsOnly: false,
@@ -65,7 +67,7 @@ export function Catalogs<T extends Constructor<BaseIterableClient>>(Base: T) {
6567
itemId: string
6668
): Promise<CatalogItemWithProperties> {
6769
const response = await this.client.get(
68-
`/api/catalogs/${catalogName}/items/${itemId}`
70+
`/api/catalogs/${encodeURIComponent(catalogName)}/items/${encodeURIComponent(itemId)}`
6971
);
7072
return this.validateResponse(response, CatalogItemWithPropertiesSchema);
7173
}
@@ -75,7 +77,7 @@ export function Catalogs<T extends Constructor<BaseIterableClient>>(Base: T) {
7577
itemId: string
7678
): Promise<IterableSuccessResponse> {
7779
const response = await this.client.delete(
78-
`/api/catalogs/${catalogName}/items/${itemId}`
80+
`/api/catalogs/${encodeURIComponent(catalogName)}/items/${encodeURIComponent(itemId)}`
7981
);
8082
return this.validateResponse(response, IterableSuccessResponseSchema);
8183
}
@@ -118,10 +120,13 @@ export function Catalogs<T extends Constructor<BaseIterableClient>>(Base: T) {
118120
);
119121
// The API returns { msg, code, params: { definedMappings, undefinedFields } }
120122
// Extract params and validate that directly
121-
return this.validateResponse(
122-
{ data: response.data.params },
123-
CatalogFieldMappingsResponseSchema
124-
);
123+
if (response.data && response.data.params) {
124+
return this.validateResponse(
125+
{ data: response.data.params },
126+
CatalogFieldMappingsResponseSchema
127+
);
128+
}
129+
return this.validateResponse(response, CatalogFieldMappingsResponseSchema);
125130
}
126131

127132
/**
@@ -151,10 +156,13 @@ export function Catalogs<T extends Constructor<BaseIterableClient>>(Base: T) {
151156
const response = await this.client.get(url);
152157
// The API returns { msg, code, params: { catalogItemsWithProperties, totalItemsCount } }
153158
// Extract params and validate that directly
154-
return this.validateResponse(
155-
{ data: response.data.params },
156-
GetCatalogItemsResponseSchema
157-
);
159+
if (response.data && response.data.params) {
160+
return this.validateResponse(
161+
{ data: response.data.params },
162+
GetCatalogItemsResponseSchema
163+
);
164+
}
165+
return this.validateResponse(response, GetCatalogItemsResponseSchema);
158166
}
159167

160168
/**

src/client/lists.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import {
88
CreateListResponseSchema,
99
GetListPreviewUsersParams,
1010
GetListPreviewUsersResponse,
11+
GetListPreviewUsersResponseSchema,
1112
GetListSizeParams,
1213
GetListSizeResponse,
14+
GetListSizeResponseSchema,
1315
GetListsResponse,
1416
GetListsResponseSchema,
1517
GetListUsersParams,
@@ -70,9 +72,14 @@ export function Lists<T extends Constructor<BaseIterableClient>>(Base: T) {
7072
.trim()
7173
.split("\n")
7274
.filter((email) => email.trim());
73-
return {
75+
const result = {
7476
users: emails.map((email) => ({ email: email.trim() })),
7577
};
78+
// Validate the constructed response
79+
return this.validateResponse(
80+
{ data: result },
81+
GetListUsersResponseSchema
82+
);
7683
}
7784

7885
// Fallback to original format if it's already JSON
@@ -96,9 +103,12 @@ export function Lists<T extends Constructor<BaseIterableClient>>(Base: T) {
96103
const response = await this.client.get(
97104
`/api/lists/${params.listId}/size`
98105
);
99-
// API returns a string, but we want to return a proper object
106+
// API returns a string number, convert to proper object and validate
100107
const size = parseInt(response.data, 10);
101-
return { size };
108+
return this.validateResponse(
109+
{ data: { size } },
110+
GetListSizeResponseSchema
111+
);
102112
}
103113

104114
/**
@@ -120,13 +130,22 @@ export function Lists<T extends Constructor<BaseIterableClient>>(Base: T) {
120130
const response = await this.client.get(
121131
`/api/lists/previewUsers?${queryParams.toString()}`
122132
);
133+
123134
// API returns plain text with users separated by newlines
124-
const usersText = response.data as string;
125-
const users = usersText
126-
.trim()
127-
.split("\n")
128-
.filter((user) => user.length > 0);
129-
return { users };
135+
const responseData = response.data;
136+
if (typeof responseData === "string") {
137+
const users = responseData
138+
.trim()
139+
.split("\n")
140+
.filter((user) => user.length > 0);
141+
return this.validateResponse(
142+
{ data: { users } },
143+
GetListPreviewUsersResponseSchema
144+
);
145+
}
146+
147+
// Fallback to JSON validation if response is not plain text
148+
return this.validateResponse(response, GetListPreviewUsersResponseSchema);
130149
}
131150
};
132151
}

tests/unit/catalogs.test.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,140 @@ describe("Catalog Operations", () => {
2828
afterEach(() => {
2929
jest.clearAllMocks();
3030
});
31+
32+
describe("createCatalog", () => {
33+
it("should create catalog with correct endpoint", async () => {
34+
const mockResponse = {
35+
data: {
36+
msg: "Catalog created successfully",
37+
code: "Success",
38+
},
39+
};
40+
mockAxiosInstance.post.mockResolvedValue(mockResponse);
41+
42+
const result = await client.createCatalog("test-catalog");
43+
44+
expect(mockAxiosInstance.post).toHaveBeenCalledWith(
45+
"/api/catalogs/test-catalog"
46+
);
47+
expect(result).toEqual(mockResponse.data);
48+
});
49+
50+
it("should encode catalog name with special characters", async () => {
51+
const mockResponse = {
52+
data: {
53+
msg: "Catalog created successfully",
54+
code: "Success",
55+
},
56+
};
57+
mockAxiosInstance.post.mockResolvedValue(mockResponse);
58+
59+
await client.createCatalog("test catalog/with+special");
60+
61+
expect(mockAxiosInstance.post).toHaveBeenCalledWith(
62+
"/api/catalogs/test%20catalog%2Fwith%2Bspecial"
63+
);
64+
});
65+
});
66+
67+
describe("updateCatalogItems", () => {
68+
it("should encode catalog name with special characters", async () => {
69+
const mockResponse = {
70+
data: {
71+
msg: "Items updated",
72+
code: "Success",
73+
},
74+
};
75+
mockAxiosInstance.post.mockResolvedValue(mockResponse);
76+
77+
await client.updateCatalogItems({
78+
catalogName: "my catalog",
79+
items: [{ id: "item1", name: "Test" }],
80+
});
81+
82+
expect(mockAxiosInstance.post).toHaveBeenCalledWith(
83+
"/api/catalogs/my%20catalog/items",
84+
expect.any(Object)
85+
);
86+
});
87+
});
88+
89+
describe("getCatalogItem", () => {
90+
it("should get item with correct endpoint", async () => {
91+
const mockResponse = {
92+
data: {
93+
catalogName: "products",
94+
itemId: "item1",
95+
lastModified: 1704067200000,
96+
size: 1024,
97+
value: { name: "Product 1" },
98+
},
99+
};
100+
mockAxiosInstance.get.mockResolvedValue(mockResponse);
101+
102+
const result = await client.getCatalogItem("products", "item1");
103+
104+
expect(mockAxiosInstance.get).toHaveBeenCalledWith(
105+
"/api/catalogs/products/items/item1"
106+
);
107+
expect(result).toEqual(mockResponse.data);
108+
});
109+
110+
it("should encode catalog name and item ID with special characters", async () => {
111+
const mockResponse = {
112+
data: {
113+
catalogName: "my catalog",
114+
itemId: "item/1",
115+
lastModified: 1704067200000,
116+
size: 1024,
117+
value: {},
118+
},
119+
};
120+
mockAxiosInstance.get.mockResolvedValue(mockResponse);
121+
122+
await client.getCatalogItem("my catalog", "item/1");
123+
124+
expect(mockAxiosInstance.get).toHaveBeenCalledWith(
125+
"/api/catalogs/my%20catalog/items/item%2F1"
126+
);
127+
});
128+
});
129+
130+
describe("deleteCatalogItem", () => {
131+
it("should delete item with correct endpoint", async () => {
132+
const mockResponse = {
133+
data: {
134+
msg: "Item deleted",
135+
code: "Success",
136+
},
137+
};
138+
mockAxiosInstance.delete.mockResolvedValue(mockResponse);
139+
140+
const result = await client.deleteCatalogItem("products", "item1");
141+
142+
expect(mockAxiosInstance.delete).toHaveBeenCalledWith(
143+
"/api/catalogs/products/items/item1"
144+
);
145+
expect(result).toEqual(mockResponse.data);
146+
});
147+
148+
it("should encode catalog name and item ID with special characters", async () => {
149+
const mockResponse = {
150+
data: {
151+
msg: "Item deleted",
152+
code: "Success",
153+
},
154+
};
155+
mockAxiosInstance.delete.mockResolvedValue(mockResponse);
156+
157+
await client.deleteCatalogItem("my catalog", "item+1");
158+
159+
expect(mockAxiosInstance.delete).toHaveBeenCalledWith(
160+
"/api/catalogs/my%20catalog/items/item%2B1"
161+
);
162+
});
163+
});
164+
31165
describe("getCatalogs", () => {
32166
it("should build pagination query parameters", async () => {
33167
const mockResponse = {

0 commit comments

Comments
 (0)