From ecc7813d67675482ede27eb835f3352787e4d195 Mon Sep 17 00:00:00 2001 From: Andrew Boni Date: Tue, 25 Nov 2025 14:15:52 -0800 Subject: [PATCH] URL encode Catalog name --- src/client/catalogs.ts | 32 +++++---- src/client/lists.ts | 37 +++++++--- tests/unit/catalogs.test.ts | 134 ++++++++++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 21 deletions(-) diff --git a/src/client/catalogs.ts b/src/client/catalogs.ts index 1fb28bb..1f3c0ab 100644 --- a/src/client/catalogs.ts +++ b/src/client/catalogs.ts @@ -29,7 +29,9 @@ import type { BaseIterableClient } from "./base.js"; export function Catalogs>(Base: T) { return class extends Base { async createCatalog(catalogName: string): Promise { - const response = await this.client.post(`/api/catalogs/${catalogName}`); + const response = await this.client.post( + `/api/catalogs/${encodeURIComponent(catalogName)}` + ); return this.validateResponse(response, IterableSuccessResponseSchema); } @@ -51,7 +53,7 @@ export function Catalogs>(Base: T) { }); const response = await this.client.post( - `/api/catalogs/${options.catalogName}/items`, + `/api/catalogs/${encodeURIComponent(options.catalogName)}/items`, { documents, replaceUploadedFieldsOnly: false, @@ -65,7 +67,7 @@ export function Catalogs>(Base: T) { itemId: string ): Promise { const response = await this.client.get( - `/api/catalogs/${catalogName}/items/${itemId}` + `/api/catalogs/${encodeURIComponent(catalogName)}/items/${encodeURIComponent(itemId)}` ); return this.validateResponse(response, CatalogItemWithPropertiesSchema); } @@ -75,7 +77,7 @@ export function Catalogs>(Base: T) { itemId: string ): Promise { const response = await this.client.delete( - `/api/catalogs/${catalogName}/items/${itemId}` + `/api/catalogs/${encodeURIComponent(catalogName)}/items/${encodeURIComponent(itemId)}` ); return this.validateResponse(response, IterableSuccessResponseSchema); } @@ -118,10 +120,13 @@ export function Catalogs>(Base: T) { ); // The API returns { msg, code, params: { definedMappings, undefinedFields } } // Extract params and validate that directly - return this.validateResponse( - { data: response.data.params }, - CatalogFieldMappingsResponseSchema - ); + if (response.data && response.data.params) { + return this.validateResponse( + { data: response.data.params }, + CatalogFieldMappingsResponseSchema + ); + } + return this.validateResponse(response, CatalogFieldMappingsResponseSchema); } /** @@ -151,10 +156,13 @@ export function Catalogs>(Base: T) { const response = await this.client.get(url); // The API returns { msg, code, params: { catalogItemsWithProperties, totalItemsCount } } // Extract params and validate that directly - return this.validateResponse( - { data: response.data.params }, - GetCatalogItemsResponseSchema - ); + if (response.data && response.data.params) { + return this.validateResponse( + { data: response.data.params }, + GetCatalogItemsResponseSchema + ); + } + return this.validateResponse(response, GetCatalogItemsResponseSchema); } /** diff --git a/src/client/lists.ts b/src/client/lists.ts index c72759e..ad7d2d8 100644 --- a/src/client/lists.ts +++ b/src/client/lists.ts @@ -8,8 +8,10 @@ import { CreateListResponseSchema, GetListPreviewUsersParams, GetListPreviewUsersResponse, + GetListPreviewUsersResponseSchema, GetListSizeParams, GetListSizeResponse, + GetListSizeResponseSchema, GetListsResponse, GetListsResponseSchema, GetListUsersParams, @@ -70,9 +72,14 @@ export function Lists>(Base: T) { .trim() .split("\n") .filter((email) => email.trim()); - return { + const result = { users: emails.map((email) => ({ email: email.trim() })), }; + // Validate the constructed response + return this.validateResponse( + { data: result }, + GetListUsersResponseSchema + ); } // Fallback to original format if it's already JSON @@ -96,9 +103,12 @@ export function Lists>(Base: T) { const response = await this.client.get( `/api/lists/${params.listId}/size` ); - // API returns a string, but we want to return a proper object + // API returns a string number, convert to proper object and validate const size = parseInt(response.data, 10); - return { size }; + return this.validateResponse( + { data: { size } }, + GetListSizeResponseSchema + ); } /** @@ -120,13 +130,22 @@ export function Lists>(Base: T) { const response = await this.client.get( `/api/lists/previewUsers?${queryParams.toString()}` ); + // API returns plain text with users separated by newlines - const usersText = response.data as string; - const users = usersText - .trim() - .split("\n") - .filter((user) => user.length > 0); - return { users }; + const responseData = response.data; + if (typeof responseData === "string") { + const users = responseData + .trim() + .split("\n") + .filter((user) => user.length > 0); + return this.validateResponse( + { data: { users } }, + GetListPreviewUsersResponseSchema + ); + } + + // Fallback to JSON validation if response is not plain text + return this.validateResponse(response, GetListPreviewUsersResponseSchema); } }; } diff --git a/tests/unit/catalogs.test.ts b/tests/unit/catalogs.test.ts index 1cbdcec..b9e63eb 100644 --- a/tests/unit/catalogs.test.ts +++ b/tests/unit/catalogs.test.ts @@ -28,6 +28,140 @@ describe("Catalog Operations", () => { afterEach(() => { jest.clearAllMocks(); }); + + describe("createCatalog", () => { + it("should create catalog with correct endpoint", async () => { + const mockResponse = { + data: { + msg: "Catalog created successfully", + code: "Success", + }, + }; + mockAxiosInstance.post.mockResolvedValue(mockResponse); + + const result = await client.createCatalog("test-catalog"); + + expect(mockAxiosInstance.post).toHaveBeenCalledWith( + "/api/catalogs/test-catalog" + ); + expect(result).toEqual(mockResponse.data); + }); + + it("should encode catalog name with special characters", async () => { + const mockResponse = { + data: { + msg: "Catalog created successfully", + code: "Success", + }, + }; + mockAxiosInstance.post.mockResolvedValue(mockResponse); + + await client.createCatalog("test catalog/with+special"); + + expect(mockAxiosInstance.post).toHaveBeenCalledWith( + "/api/catalogs/test%20catalog%2Fwith%2Bspecial" + ); + }); + }); + + describe("updateCatalogItems", () => { + it("should encode catalog name with special characters", async () => { + const mockResponse = { + data: { + msg: "Items updated", + code: "Success", + }, + }; + mockAxiosInstance.post.mockResolvedValue(mockResponse); + + await client.updateCatalogItems({ + catalogName: "my catalog", + items: [{ id: "item1", name: "Test" }], + }); + + expect(mockAxiosInstance.post).toHaveBeenCalledWith( + "/api/catalogs/my%20catalog/items", + expect.any(Object) + ); + }); + }); + + describe("getCatalogItem", () => { + it("should get item with correct endpoint", async () => { + const mockResponse = { + data: { + catalogName: "products", + itemId: "item1", + lastModified: 1704067200000, + size: 1024, + value: { name: "Product 1" }, + }, + }; + mockAxiosInstance.get.mockResolvedValue(mockResponse); + + const result = await client.getCatalogItem("products", "item1"); + + expect(mockAxiosInstance.get).toHaveBeenCalledWith( + "/api/catalogs/products/items/item1" + ); + expect(result).toEqual(mockResponse.data); + }); + + it("should encode catalog name and item ID with special characters", async () => { + const mockResponse = { + data: { + catalogName: "my catalog", + itemId: "item/1", + lastModified: 1704067200000, + size: 1024, + value: {}, + }, + }; + mockAxiosInstance.get.mockResolvedValue(mockResponse); + + await client.getCatalogItem("my catalog", "item/1"); + + expect(mockAxiosInstance.get).toHaveBeenCalledWith( + "/api/catalogs/my%20catalog/items/item%2F1" + ); + }); + }); + + describe("deleteCatalogItem", () => { + it("should delete item with correct endpoint", async () => { + const mockResponse = { + data: { + msg: "Item deleted", + code: "Success", + }, + }; + mockAxiosInstance.delete.mockResolvedValue(mockResponse); + + const result = await client.deleteCatalogItem("products", "item1"); + + expect(mockAxiosInstance.delete).toHaveBeenCalledWith( + "/api/catalogs/products/items/item1" + ); + expect(result).toEqual(mockResponse.data); + }); + + it("should encode catalog name and item ID with special characters", async () => { + const mockResponse = { + data: { + msg: "Item deleted", + code: "Success", + }, + }; + mockAxiosInstance.delete.mockResolvedValue(mockResponse); + + await client.deleteCatalogItem("my catalog", "item+1"); + + expect(mockAxiosInstance.delete).toHaveBeenCalledWith( + "/api/catalogs/my%20catalog/items/item%2B1" + ); + }); + }); + describe("getCatalogs", () => { it("should build pagination query parameters", async () => { const mockResponse = {