Skip to content

Commit 5949080

Browse files
authored
improve csv validation (#1)
1 parent cc80d64 commit 5949080

File tree

7 files changed

+42
-14
lines changed

7 files changed

+42
-14
lines changed

src/client/base.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import https from "node:https";
44
import { dirname, join } from "node:path";
55
import { fileURLToPath } from "node:url";
66

7-
import axios, { AxiosInstance } from "axios";
7+
import axios, { AxiosInstance, AxiosResponse } from "axios";
88
import { parse as csvParse } from "csv-parse/sync";
99
import { z } from "zod";
1010

@@ -153,24 +153,28 @@ export class BaseIterableClient {
153153

154154
/**
155155
* Parse CSV response into an array of objects using csv-parse library
156+
* @throws IterableResponseValidationError if CSV parsing fails
156157
*/
157-
public parseCsv(data: string): any[] {
158-
if (!data) {
158+
public parseCsv(response: AxiosResponse<string>): Record<string, string>[] {
159+
if (!response.data) {
159160
return [];
160161
}
161162

162163
try {
163-
return csvParse(data, {
164+
return csvParse(response.data, {
164165
columns: true, // Use first line as headers
165166
skip_empty_lines: true,
166167
trim: true,
167168
});
168169
} catch (error) {
169-
// Log error but don't throw - return empty array for graceful degradation
170-
logger.warn("Failed to parse CSV data", {
171-
error: error instanceof Error ? error.message : String(error),
172-
});
173-
return [];
170+
// Throw validation error to maintain consistent error handling
171+
// This allows callers to handle parse failures appropriately
172+
throw new IterableResponseValidationError(
173+
200,
174+
response.data,
175+
`CSV parse error: ${error instanceof Error ? error.message : String(error)}`,
176+
response.config?.url
177+
);
174178
}
175179
}
176180

src/client/campaigns.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ArchiveCampaignsParams,
55
ArchiveCampaignsResponse,
66
ArchiveCampaignsResponseSchema,
7+
CampaignMetricsResponse,
78
CancelCampaignParams,
89
CreateCampaignParams,
910
CreateCampaignResponse,
@@ -111,7 +112,7 @@ export function Campaigns<T extends Constructor<BaseIterableClient>>(Base: T) {
111112

112113
async getCampaignMetrics(
113114
options: GetCampaignMetricsParams
114-
): Promise<any[]> {
115+
): Promise<CampaignMetricsResponse> {
115116
const params = new URLSearchParams();
116117
params.append("campaignId", options.campaignId.toString());
117118
if (options.startDateTime)
@@ -127,7 +128,7 @@ export function Campaigns<T extends Constructor<BaseIterableClient>>(Base: T) {
127128
);
128129

129130
// Parse CSV response into array of objects
130-
return this.parseCsv(response.data);
131+
return this.parseCsv(response);
131132
}
132133

133134
async scheduleCampaign(

src/client/experiments.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export function Experiments<T extends Constructor<BaseIterableClient>>(
4242
});
4343

4444
// Parse CSV response into array of objects
45-
return this.parseCsv(response.data);
45+
return this.parseCsv(response);
4646
}
4747
};
4848
}

src/types/campaigns.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ export type GetCampaignResponse = z.infer<typeof GetCampaignResponseSchema>;
114114
export type GetCampaignMetricsParams = z.infer<
115115
typeof GetCampaignMetricsParamsSchema
116116
>;
117+
export type CampaignMetricsResponse = z.infer<
118+
typeof CampaignMetricsResponseSchema
119+
>;
117120
export type CreateCampaignParams = z.infer<typeof CreateCampaignParamsSchema>;
118121
export type CreateCampaignResponse = z.infer<
119122
typeof CreateCampaignResponseSchema
@@ -136,8 +139,10 @@ export const CampaignsResponseSchema = z.object({
136139
campaigns: z.array(CampaignDetailsSchema),
137140
});
138141

142+
// The API returns CSV data which we parse into objects
143+
// All CSV values are strings
139144
export const CampaignMetricsResponseSchema = z
140-
.array(z.record(z.string(), z.any()))
145+
.array(z.record(z.string(), z.string()))
141146
.describe("Parsed campaign metrics data");
142147

143148
// Campaign creation schemas

src/types/experiments.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { IterableDateTimeSchema } from "./common.js";
88

99
// The API returns CSV data which we parse into objects
1010
export const ExperimentMetricsResponseSchema = z
11-
.array(z.record(z.string(), z.any()))
11+
.array(z.record(z.string(), z.string()))
1212
.describe("Parsed experiment metrics data");
1313

1414
export const GetExperimentMetricsParamsSchema = z

tests/unit/campaigns.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,15 @@ describe("Campaign Management", () => {
288288
{ responseType: "text" }
289289
);
290290
});
291+
292+
it("should throw IterableResponseValidationError for invalid CSV", async () => {
293+
const mockResponse = { data: "invalid,csv\n\ndata" };
294+
mockAxiosInstance.get.mockResolvedValue(mockResponse);
295+
296+
await expect(
297+
client.getCampaignMetrics({ campaignId: 12345 })
298+
).rejects.toThrow("CSV parse error");
299+
});
291300
});
292301

293302
describe("getChildCampaigns", () => {

tests/unit/experiments.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,5 +217,14 @@ describe("Experiment Operations", () => {
217217
expect(result).toEqual(expectedParsedData);
218218
expect(Array.isArray(result)).toBe(true);
219219
});
220+
221+
it("should throw IterableResponseValidationError for invalid CSV", async () => {
222+
const mockResponse = { data: "invalid,csv\n\ndata" };
223+
mockAxiosInstance.get.mockResolvedValue(mockResponse);
224+
225+
await expect(client.getExperimentMetrics()).rejects.toThrow(
226+
"CSV parse error"
227+
);
228+
});
220229
});
221230
});

0 commit comments

Comments
 (0)