Skip to content

Commit 11b93fb

Browse files
committed
feat(openapi-fetch): add transform options for response data handling
1 parent 7f3f7b6 commit 11b93fb

6 files changed

Lines changed: 238 additions & 1 deletion

File tree

packages/openapi-fetch/src/index.d.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export interface ClientOptions extends Omit<RequestInit, "headers"> {
2323
querySerializer?: QuerySerializer<unknown> | QuerySerializerOptions;
2424
/** global bodySerializer */
2525
bodySerializer?: BodySerializer<unknown>;
26+
/** transform functions for request/response data */
27+
transform?: TransformOptions<unknown, unknown>;
2628
headers?: HeadersOptions;
2729
/** RequestInit extension object to pass as 2nd argument to fetch when supported (defaults to undefined) */
2830
requestInitExt?: Record<string, unknown>;
@@ -64,6 +66,18 @@ export type QuerySerializerOptions = {
6466

6567
export type BodySerializer<T> = (body: OperationRequestBodyContent<T>) => any;
6668

69+
export type TransformOptions<T = any, R = any> = {
70+
response?: (method: string, path: string, data: T) => R;
71+
};
72+
73+
export type TransformFunction<T = any, R = any> = (
74+
method: string,
75+
path: string,
76+
options: {
77+
data: T;
78+
},
79+
) => R;
80+
6781
type BodyType<T = unknown> = {
6882
json: T;
6983
text: Awaited<ReturnType<Response["text"]>>;
@@ -127,6 +141,7 @@ export type MergedOptions<T = unknown> = {
127141
parseAs: ParseAs;
128142
querySerializer: QuerySerializer<T>;
129143
bodySerializer: BodySerializer<T>;
144+
transform?: TransformOptions<T, T>;
130145
fetch: typeof globalThis.fetch;
131146
};
132147

packages/openapi-fetch/src/index.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export default function createClient(clientOptions) {
2828
fetch: baseFetch = globalThis.fetch,
2929
querySerializer: globalQuerySerializer,
3030
bodySerializer: globalBodySerializer,
31+
transform: globalTransform,
3132
headers: baseHeaders,
3233
requestInitExt = undefined,
3334
...baseOptions
@@ -114,6 +115,7 @@ export default function createClient(clientOptions) {
114115
parseAs,
115116
querySerializer,
116117
bodySerializer,
118+
transform: globalTransform,
117119
});
118120
for (const m of middlewares) {
119121
if (m && typeof m === "object" && typeof m.onRequest === "function") {
@@ -219,7 +221,14 @@ export default function createClient(clientOptions) {
219221
if (parseAs === "stream") {
220222
return { data: response.body, response };
221223
}
222-
return { data: await response[parseAs](), response };
224+
225+
let responseData = await response[parseAs]();
226+
227+
if (globalTransform?.response && responseData !== undefined) {
228+
responseData = globalTransform.response(request.method, schemaPath, responseData);
229+
}
230+
231+
return { data: responseData, response };
223232
}
224233

225234
// handle errors

packages/openapi-fetch/test/redocly.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ apis:
4545
root: ./path-based-client/schemas/path-based-client.yaml
4646
x-openapi-ts:
4747
output: ./path-based-client/schemas/path-based-client.d.ts
48+
transform:
49+
root: ./transform/schemas/transform.yaml
50+
x-openapi-ts:
51+
output: ./transform/schemas/transform.d.ts
4852
github:
4953
root: ../../openapi-typescript/examples/github-api.yaml
5054
x-openapi-ts:
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* This file was manually created based on transform.yaml
3+
*/
4+
5+
import type { PathsWithMethod } from "openapi-typescript-helpers";
6+
7+
export interface paths {
8+
"/posts": {
9+
get: {
10+
responses: {
11+
200: {
12+
content: {
13+
"application/json": {
14+
items: any[];
15+
meta: {
16+
total: number;
17+
};
18+
};
19+
};
20+
};
21+
};
22+
};
23+
post: {
24+
requestBody: {
25+
content: {
26+
"application/json": {
27+
title: string;
28+
content: string;
29+
};
30+
};
31+
};
32+
responses: {
33+
200: {
34+
content: {
35+
"application/json": {
36+
id: number;
37+
name: string;
38+
created_at: string;
39+
updated_at: string;
40+
};
41+
};
42+
};
43+
};
44+
};
45+
};
46+
"/posts/{id}": {
47+
get: {
48+
parameters: {
49+
path: {
50+
id: number;
51+
};
52+
};
53+
responses: {
54+
200: {
55+
content: {
56+
"application/json": {
57+
id: number;
58+
title: string;
59+
content: string;
60+
created_at: string;
61+
updated_at: string;
62+
};
63+
};
64+
};
65+
};
66+
};
67+
};
68+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
openapi: 3.0.0
2+
info:
3+
title: Transform Test API
4+
version: 1.0.0
5+
paths:
6+
/posts:
7+
get:
8+
summary: Get all posts
9+
responses:
10+
'200':
11+
description: A list of posts
12+
content:
13+
application/json:
14+
schema:
15+
type: object
16+
properties:
17+
items:
18+
type: array
19+
items:
20+
type: object
21+
properties:
22+
id:
23+
type: integer
24+
name:
25+
type: string
26+
description:
27+
type: string
28+
sensitive:
29+
type: string
30+
created_at:
31+
type: string
32+
format: date-time
33+
meta:
34+
type: object
35+
properties:
36+
total:
37+
type: integer
38+
post:
39+
summary: Create a new post
40+
requestBody:
41+
content:
42+
application/json:
43+
schema:
44+
type: object
45+
properties:
46+
title:
47+
type: string
48+
content:
49+
type: string
50+
responses:
51+
'200':
52+
description: The created post
53+
content:
54+
application/json:
55+
schema:
56+
type: object
57+
properties:
58+
id:
59+
type: integer
60+
name:
61+
type: string
62+
created_at:
63+
type: string
64+
format: date-time
65+
updated_at:
66+
type: string
67+
format: date-time
68+
/posts/{id}:
69+
get:
70+
summary: Get a post by ID
71+
parameters:
72+
- name: id
73+
in: path
74+
required: true
75+
schema:
76+
type: integer
77+
responses:
78+
'200':
79+
description: A post
80+
content:
81+
application/json:
82+
schema:
83+
type: object
84+
properties:
85+
id:
86+
type: integer
87+
title:
88+
type: string
89+
content:
90+
type: string
91+
created_at:
92+
type: string
93+
format: date-time
94+
updated_at:
95+
type: string
96+
format: date-time
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { assert, expect, test } from "vitest";
2+
import { createObservedClient } from "../helpers.js";
3+
import type { paths } from "./schemas/transform.js";
4+
5+
interface PostResponse {
6+
id: number;
7+
title: string;
8+
created_at: string | Date;
9+
}
10+
11+
test("transforms date strings to Date objects", async () => {
12+
const client = createObservedClient<paths>(
13+
{
14+
transform: {
15+
response: (method, path, data) => {
16+
if (!data || typeof data !== "object") return data;
17+
18+
const result = { ...data } as PostResponse;
19+
20+
if (typeof result.created_at === "string") {
21+
result.created_at = new Date(result.created_at);
22+
}
23+
24+
return result;
25+
}
26+
}
27+
},
28+
async () => Response.json({
29+
id: 1,
30+
title: "Test Post",
31+
created_at: "2023-01-01T00:00:00Z"
32+
})
33+
);
34+
35+
const { data } = await client.GET("/posts/{id}", {
36+
params: { path: { id: 1 } }
37+
});
38+
39+
const post = data as PostResponse;
40+
41+
assert(post.created_at instanceof Date, "created_at should be a Date");
42+
expect(post.created_at.getFullYear()).toBe(2023);
43+
expect(post.created_at.getMonth()).toBe(0); // January
44+
expect(post.created_at.getDate()).toBe(1);
45+
});

0 commit comments

Comments
 (0)