Skip to content

Commit d3c16ba

Browse files
committed
feat(api): enhance client creation with ClientOptions type for better type safety
fix(api): handle HTTPPayloadError in PetDetail component for improved error handling chore(tests): update generator tests to check for ClientOptions type in generated client code fix(core): add beforeError hook to ApiClient for better error handling fix(core): improve type definitions for ClientOptions and HTTPPayloadError chore(tests): mock HTTPError in tests for consistent error handling chore(tsconfig): refine exclude patterns to prevent unnecessary files from being included
1 parent 0f62d60 commit d3c16ba

File tree

9 files changed

+99
-27
lines changed

9 files changed

+99
-27
lines changed

examples/hooks/src/api/client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { z } from "zod";
2-
import { createClient as createRuntimeClient } from "@zoddy/core";
2+
import { createClient as createRuntimeClient, type ClientOptions } from "@zoddy/core";
33

44
export const Order = z.object({
55
id: z.number().int().optional(),
@@ -834,6 +834,6 @@ export const operations = {
834834

835835
export type Operations = typeof operations;
836836

837-
export function createClient(baseUrl: string, options?: { headers?: Record<string, string> }) {
837+
export function createClient(baseUrl: string, options?: ClientOptions) {
838838
return createRuntimeClient(baseUrl, operations, options);
839839
}

examples/hooks/src/components/PetDetail.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ParamsById, QueriesById } from "@zoddy/core";
1+
import { HTTPPayloadError, type ParamsById, type QueriesById } from "@zoddy/core";
22
import type { operations } from "../api/client";
33
import { hooks } from "../api/hooks";
44

@@ -15,16 +15,25 @@ export const PetDetail = ({
1515
isRefetching,
1616
refetch,
1717
error,
18-
} = hooks.useGetPetById({
19-
params,
20-
queries,
21-
});
18+
} = hooks.useGetPetById(
19+
{
20+
params,
21+
queries,
22+
},
23+
{
24+
retry: false,
25+
}
26+
);
2227

2328
if (isPending) {
2429
return <div>Loading...</div>;
2530
}
2631

2732
if (error) {
33+
if (error instanceof HTTPPayloadError) {
34+
return <div>Error: {JSON.stringify(error.payload)}</div>;
35+
}
36+
console.error(error);
2837
return <div>Error: {error.message}</div>;
2938
}
3039

examples/hooks/src/components/UpdatePet.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ export const UpdatePetForm = ({
2525
console.log("Invalidating getPetById", hooks.getKey("getPetById", { params }));
2626
queryClient.invalidateQueries({ queryKey: hooks.getKey("getPetById", { params }) });
2727
},
28-
onError: (error) => {
29-
console.error("Error updating pet:", error);
30-
},
3128
});
3229

3330
const handleSubmit = (e: React.FormEvent) => {

packages/cli/src/generator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ export async function generateClient(oas: OpenAPIV3.Document): Promise<string> {
2020
const schemaCode = generateSchemaCode(schemas);
2121
const operationsCode = generateOperationsCode(operations);
2222
const code = `import { z } from 'zod';
23-
import { createClient as createRuntimeClient } from '@zoddy/core';
23+
import { createClient as createRuntimeClient, type ClientOptions } from '@zoddy/core';
2424
2525
${schemaCode}
2626
2727
${operationsCode}
2828
29-
export function createClient(baseUrl: string, options?: { headers?: Record<string, string> }) {
29+
export function createClient(baseUrl: string, options?: ClientOptions) {
3030
return createRuntimeClient(baseUrl, operations, options);
3131
}
3232
`;

packages/cli/tests/generator.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ describe("zoddy generator", () => {
3434
it("should generate client code with correct imports", () => {
3535
expect(clientCode).toContain('import { z } from "zod"');
3636
expect(clientCode).toContain(
37-
'import { createClient as createRuntimeClient } from "@zoddy/core"'
37+
'createClient as createRuntimeClient'
3838
);
39+
expect(clientCode).toContain('type ClientOptions')
40+
expect(clientCode).toContain('@zoddy/core')
3941
});
4042

4143
it("should generate schema exports", () => {

packages/core/src/index.ts

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
1-
import ky, { type Hooks } from "ky";
1+
import ky, {
2+
type Hooks,
3+
type Options,
4+
type NormalizedOptions,
5+
type BeforeErrorHook,
6+
HTTPError,
7+
} from "ky";
28
import type { ZodError, ZodType } from "zod";
39

4-
export interface ClientOptions {
10+
// Ky types
11+
export type { Hooks, Options };
12+
13+
export class HTTPPayloadError extends HTTPError {
14+
payload: any;
15+
constructor(response: Response, request: Request, options: NormalizedOptions, payload: any) {
16+
super(response, request, options);
17+
this.payload = payload;
18+
}
19+
}
20+
21+
export { HTTPError };
22+
23+
export interface ClientOptions extends Options {
524
baseUrl: string;
6-
headers?: Record<string, string>;
725
validate?: boolean;
8-
hooks?: Hooks;
9-
validationHelpers?: ValidationHelpers;
26+
disableErrorParsing?: boolean;
1027
}
1128

1229
export interface OperationParameter {
@@ -413,38 +430,60 @@ export class ApiClient {
413430
private validationHelpers?: ValidationHelpers;
414431

415432
constructor(baseUrl: string, operations: Operations, options?: Omit<ClientOptions, "baseUrl">) {
433+
const { validate, disableErrorParsing, ...kyOptions } = options ?? {};
416434
this.operations = operations;
417435

418-
const shouldValidate = options?.validate !== false;
436+
const shouldValidate = validate !== false;
437+
438+
const beforeError: BeforeErrorHook = async (error) => {
439+
const { response, request, options } = error;
440+
try {
441+
const newError = new HTTPPayloadError(response, request, options, await response.json());
442+
return newError;
443+
} catch (e) {
444+
return error;
445+
}
446+
};
419447

420448
if (!shouldValidate) {
421449
this.ky = ky.create({
422450
prefixUrl: baseUrl,
423-
...options,
451+
...kyOptions,
452+
hooks: {
453+
...kyOptions.hooks,
454+
beforeError: [
455+
...(disableErrorParsing ? [beforeError] : []),
456+
...(kyOptions.hooks?.beforeError ?? []),
457+
],
458+
},
424459
});
425460
return;
426461
}
427462

428-
this.validationHelpers = options?.validationHelpers ?? createValidationHelpers();
463+
this.validationHelpers = createValidationHelpers();
429464
const hooks = createKyValidationHooks(this.validationHelpers);
430465

431466
this.ky = ky.create({
432467
prefixUrl: baseUrl,
433-
...(options?.headers && { headers: options.headers }),
468+
...kyOptions,
434469
hooks: {
470+
beforeError: [
471+
...(disableErrorParsing ? [beforeError] : []),
472+
...(kyOptions.hooks?.beforeError ?? []),
473+
],
435474
beforeRequest: [
436475
(request, options) => {
437476
const operation = (options as any).operation as Operation;
438477
return hooks.beforeRequest?.(request, options, operation) ?? request;
439478
},
440-
...(options?.hooks?.beforeRequest ?? []),
479+
...(kyOptions?.hooks?.beforeRequest ?? []),
441480
],
442481
afterResponse: [
443482
(request, options, response) => {
444483
const operation = (options as any).operation as Operation;
445484
return hooks.afterResponse?.(request, options, response, operation) ?? response;
446485
},
447-
...(options?.hooks?.afterResponse ?? []),
486+
...(kyOptions?.hooks?.afterResponse ?? []),
448487
],
449488
},
450489
});
@@ -497,7 +536,7 @@ export class ApiClient {
497536
}
498537
}
499538

500-
const requestOptions: any = {
539+
const requestOptions: Record<string, any> = {
501540
method: operation.method.toUpperCase(),
502541
headers,
503542
searchParams,

packages/core/tests/api-client.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,18 @@ import { z } from "zod";
33
import { ApiClient, type Operations, createClient } from "../src/index";
44

55
// Mock ky for testing
6-
vi.mock("ky", () => {
6+
vi.mock("ky", async () => {
7+
// Create a mock HTTPError class
8+
class MockHTTPError extends Error {
9+
constructor(
10+
public response: Response,
11+
public request: Request,
12+
public options: any
13+
) {
14+
super("HTTP Error");
15+
}
16+
}
17+
718
const mockResponse = {
819
json: vi.fn(),
920
text: vi.fn(),
@@ -19,6 +30,7 @@ vi.mock("ky", () => {
1930

2031
return {
2132
default: mockKy,
33+
HTTPError: MockHTTPError,
2234
};
2335
});
2436

@@ -129,6 +141,7 @@ describe("ApiClient", () => {
129141
hooks: {
130142
beforeRequest: expect.any(Array),
131143
afterResponse: expect.any(Array),
144+
beforeError: expect.any(Array),
132145
},
133146
});
134147
});

packages/core/tests/integration.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ import {
1010

1111
// Mock ky for integration testing
1212
vi.mock("ky", () => {
13+
// Create a mock HTTPError class
14+
class MockHTTPError extends Error {
15+
constructor(
16+
public response: Response,
17+
public request: Request,
18+
public options: any
19+
) {
20+
super("HTTP Error");
21+
}
22+
}
23+
1324
let currentHooks: any = null;
1425

1526
const mockKy = vi.fn().mockImplementation(async () => {
@@ -60,6 +71,7 @@ vi.mock("ky", () => {
6071

6172
return {
6273
default: mockKy,
74+
HTTPError: MockHTTPError,
6375
};
6476
});
6577

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@
3333
"incremental": true
3434
},
3535
"include": ["src/**/*"],
36-
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
36+
"exclude": ["node_modules", "**/node_modules/**", "dist", "**/*.test.ts", "**/*.spec.ts"]
3737
}

0 commit comments

Comments
 (0)