From 3913105cdae4591a36cf7b20a1bc3b0741efe650 Mon Sep 17 00:00:00 2001 From: Nitesh Sandal Date: Fri, 10 Apr 2026 10:37:26 -0400 Subject: [PATCH] correctly send multiple expands as a comma seperated array --- .fernignore | 3 +- src/core/fetcher/createRequestUrl.ts | 22 ++++- tests/unit/fetcher/createRequestUrl.test.ts | 6 +- tests/unit/multiple-expand-handling.test.ts | 91 +++++++++++++++++++++ 4 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 tests/unit/multiple-expand-handling.test.ts diff --git a/.fernignore b/.fernignore index 7221544f8..dac2a1c06 100644 --- a/.fernignore +++ b/.fernignore @@ -17,4 +17,5 @@ tests/integration/backward-compatibility.integration.test.ts src/index.ts tests/unit/schemas/enum/enum.test.ts tests/unit/schemas/union/union.test.ts -src/core/schemas/builders/enum/enum.ts \ No newline at end of file +src/core/schemas/builders/enum/enum.ts +src/core/fetcher/createRequestUrl.ts \ No newline at end of file diff --git a/src/core/fetcher/createRequestUrl.ts b/src/core/fetcher/createRequestUrl.ts index 4bcb19854..799ac5ed7 100644 --- a/src/core/fetcher/createRequestUrl.ts +++ b/src/core/fetcher/createRequestUrl.ts @@ -1,6 +1,26 @@ import { toQueryString } from "../url/qs"; export function createRequestUrl(baseUrl: string, queryParameters?: Record): string { - const queryString = toQueryString(queryParameters, { arrayFormat: "repeat" }); + // Flatten any array values to comma-separated strings. + // Merge's REST API expects multi-value params (e.g. expand) as CSV, + // not as repeated query params. + const flattenedParams = queryParameters != null ? flattenArrayQueryParams(queryParameters) : undefined; + const queryString = toQueryString(flattenedParams, { arrayFormat: "repeat" }); return queryString ? `${baseUrl}?${queryString}` : baseUrl; } + +function flattenArrayQueryParams(params: Record): Record { + const result: Record = {}; + for (const [key, value] of Object.entries(params)) { + if ( + Array.isArray(value) && + value.length > 0 && + value.every((item) => typeof item !== "object" || item === null) + ) { + result[key] = value.join(","); + } else { + result[key] = value; + } + } + return result; +} diff --git a/tests/unit/fetcher/createRequestUrl.test.ts b/tests/unit/fetcher/createRequestUrl.test.ts index a92f1b5e8..4b1e0ef7b 100644 --- a/tests/unit/fetcher/createRequestUrl.test.ts +++ b/tests/unit/fetcher/createRequestUrl.test.ts @@ -23,10 +23,10 @@ describe("Test createRequestUrl", () => { expected: "https://api.example.com?key=value&another=param", }, { - description: "should handle array query parameters", + description: "should handle array query parameters as comma-separated values", baseUrl: BASE_URL, queryParams: { items: ["a", "b", "c"] }, - expected: "https://api.example.com?items=a&items=b&items=c", + expected: "https://api.example.com?items=a%2Cb%2Cc", }, { description: "should handle object query parameters", @@ -42,7 +42,7 @@ describe("Test createRequestUrl", () => { array: ["x", "y"], object: { key: "value" }, }, - expected: "https://api.example.com?simple=value&array=x&array=y&object%5Bkey%5D=value", + expected: "https://api.example.com?simple=value&array=x%2Cy&object%5Bkey%5D=value", }, { description: "should handle empty query parameters object", diff --git a/tests/unit/multiple-expand-handling.test.ts b/tests/unit/multiple-expand-handling.test.ts new file mode 100644 index 000000000..53444fa33 --- /dev/null +++ b/tests/unit/multiple-expand-handling.test.ts @@ -0,0 +1,91 @@ +import { createRequestUrl } from "../../src/core/fetcher/createRequestUrl"; + +describe("Multiple Expand Parameter Handling", () => { + describe("createRequestUrl flattens arrays to CSV", () => { + it("should join array values with commas instead of repeating params", () => { + const url = createRequestUrl("https://api.merge.dev/hris/v1/employees", { + expand: ["groups", "work_location"], + include_remote_data: true, + }); + expect(url).toBe( + "https://api.merge.dev/hris/v1/employees?expand=groups%2Cwork_location&include_remote_data=true" + ); + }); + + it("should handle single expand value (not array)", () => { + const url = createRequestUrl("https://api.merge.dev/hris/v1/employees", { + expand: "groups", + }); + expect(url).toBe("https://api.merge.dev/hris/v1/employees?expand=groups"); + }); + + it("should handle three or more expand values", () => { + const url = createRequestUrl("https://api.merge.dev/accounting/v1/contacts", { + expand: ["addresses", "phone_numbers", "company"], + }); + expect(url).toBe( + "https://api.merge.dev/accounting/v1/contacts?expand=addresses%2Cphone_numbers%2Ccompany" + ); + }); + + it("should handle undefined expand", () => { + const url = createRequestUrl("https://api.merge.dev/hris/v1/employees", { + expand: undefined, + cursor: "abc123", + }); + expect(url).toBe("https://api.merge.dev/hris/v1/employees?cursor=abc123"); + }); + }); + + describe("SDK client serializes expand correctly", () => { + const mockOptions = { + apiKey: "test-api-key", + environment: "https://api.merge.dev", + }; + + it("should produce correct URL with array expand via fetcher", async () => { + const { Accounting } = require("../../src"); + + let capturedUrl: string | undefined; + const mockFetcher = jest.fn().mockImplementation((args: any) => { + // Reconstruct the URL as fetcherImpl would + capturedUrl = createRequestUrl(args.url, args.queryParameters); + return { ok: true, body: { results: [] } }; + }); + + const accounting = new Accounting({ + ...mockOptions, + fetcher: mockFetcher, + }); + + await accounting.contacts.list({ + expand: ["addresses", "phone_numbers", "company"], + }); + + expect(capturedUrl).toContain("expand=addresses%2Cphone_numbers%2Ccompany"); + }); + + it("should produce correct URL with single expand via fetcher", async () => { + const { Accounting } = require("../../src"); + + let capturedUrl: string | undefined; + const mockFetcher = jest.fn().mockImplementation((args: any) => { + capturedUrl = createRequestUrl(args.url, args.queryParameters); + return { ok: true, body: { results: [] } }; + }); + + const accounting = new Accounting({ + ...mockOptions, + fetcher: mockFetcher, + }); + + await accounting.contacts.list({ + expand: "addresses", + }); + + expect(capturedUrl).toContain("expand=addresses"); + // Should NOT contain a comma since it's a single value + expect(capturedUrl).not.toContain("%2C"); + }); + }); +});