From 11b93fb41d324ee65936f40ef80bc833e55323aa Mon Sep 17 00:00:00 2001 From: Eliya Cohen Date: Mon, 14 Apr 2025 02:05:10 +0300 Subject: [PATCH 1/6] feat(openapi-fetch): add transform options for response data handling --- packages/openapi-fetch/src/index.d.ts | 15 +++ packages/openapi-fetch/src/index.js | 11 ++- packages/openapi-fetch/test/redocly.yaml | 4 + .../test/transform/schemas/transform.d.ts | 68 +++++++++++++ .../test/transform/schemas/transform.yaml | 96 +++++++++++++++++++ .../test/transform/transform.test.ts | 45 +++++++++ 6 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 packages/openapi-fetch/test/transform/schemas/transform.d.ts create mode 100644 packages/openapi-fetch/test/transform/schemas/transform.yaml create mode 100644 packages/openapi-fetch/test/transform/transform.test.ts diff --git a/packages/openapi-fetch/src/index.d.ts b/packages/openapi-fetch/src/index.d.ts index 79dec3d77..f69a3c32c 100644 --- a/packages/openapi-fetch/src/index.d.ts +++ b/packages/openapi-fetch/src/index.d.ts @@ -23,6 +23,8 @@ export interface ClientOptions extends Omit { querySerializer?: QuerySerializer | QuerySerializerOptions; /** global bodySerializer */ bodySerializer?: BodySerializer; + /** transform functions for request/response data */ + transform?: TransformOptions; headers?: HeadersOptions; /** RequestInit extension object to pass as 2nd argument to fetch when supported (defaults to undefined) */ requestInitExt?: Record; @@ -64,6 +66,18 @@ export type QuerySerializerOptions = { export type BodySerializer = (body: OperationRequestBodyContent) => any; +export type TransformOptions = { + response?: (method: string, path: string, data: T) => R; +}; + +export type TransformFunction = ( + method: string, + path: string, + options: { + data: T; + }, +) => R; + type BodyType = { json: T; text: Awaited>; @@ -127,6 +141,7 @@ export type MergedOptions = { parseAs: ParseAs; querySerializer: QuerySerializer; bodySerializer: BodySerializer; + transform?: TransformOptions; fetch: typeof globalThis.fetch; }; diff --git a/packages/openapi-fetch/src/index.js b/packages/openapi-fetch/src/index.js index ad83112e0..d7a048ef6 100644 --- a/packages/openapi-fetch/src/index.js +++ b/packages/openapi-fetch/src/index.js @@ -28,6 +28,7 @@ export default function createClient(clientOptions) { fetch: baseFetch = globalThis.fetch, querySerializer: globalQuerySerializer, bodySerializer: globalBodySerializer, + transform: globalTransform, headers: baseHeaders, requestInitExt = undefined, ...baseOptions @@ -114,6 +115,7 @@ export default function createClient(clientOptions) { parseAs, querySerializer, bodySerializer, + transform: globalTransform, }); for (const m of middlewares) { if (m && typeof m === "object" && typeof m.onRequest === "function") { @@ -219,7 +221,14 @@ export default function createClient(clientOptions) { if (parseAs === "stream") { return { data: response.body, response }; } - return { data: await response[parseAs](), response }; + + let responseData = await response[parseAs](); + + if (globalTransform?.response && responseData !== undefined) { + responseData = globalTransform.response(request.method, schemaPath, responseData); + } + + return { data: responseData, response }; } // handle errors diff --git a/packages/openapi-fetch/test/redocly.yaml b/packages/openapi-fetch/test/redocly.yaml index 6030cfa41..efdf918bc 100644 --- a/packages/openapi-fetch/test/redocly.yaml +++ b/packages/openapi-fetch/test/redocly.yaml @@ -45,6 +45,10 @@ apis: root: ./path-based-client/schemas/path-based-client.yaml x-openapi-ts: output: ./path-based-client/schemas/path-based-client.d.ts + transform: + root: ./transform/schemas/transform.yaml + x-openapi-ts: + output: ./transform/schemas/transform.d.ts github: root: ../../openapi-typescript/examples/github-api.yaml x-openapi-ts: diff --git a/packages/openapi-fetch/test/transform/schemas/transform.d.ts b/packages/openapi-fetch/test/transform/schemas/transform.d.ts new file mode 100644 index 000000000..1f1413658 --- /dev/null +++ b/packages/openapi-fetch/test/transform/schemas/transform.d.ts @@ -0,0 +1,68 @@ +/** + * This file was manually created based on transform.yaml + */ + +import type { PathsWithMethod } from "openapi-typescript-helpers"; + +export interface paths { + "/posts": { + get: { + responses: { + 200: { + content: { + "application/json": { + items: any[]; + meta: { + total: number; + }; + }; + }; + }; + }; + }; + post: { + requestBody: { + content: { + "application/json": { + title: string; + content: string; + }; + }; + }; + responses: { + 200: { + content: { + "application/json": { + id: number; + name: string; + created_at: string; + updated_at: string; + }; + }; + }; + }; + }; + }; + "/posts/{id}": { + get: { + parameters: { + path: { + id: number; + }; + }; + responses: { + 200: { + content: { + "application/json": { + id: number; + title: string; + content: string; + created_at: string; + updated_at: string; + }; + }; + }; + }; + }; + }; +} \ No newline at end of file diff --git a/packages/openapi-fetch/test/transform/schemas/transform.yaml b/packages/openapi-fetch/test/transform/schemas/transform.yaml new file mode 100644 index 000000000..8e333411f --- /dev/null +++ b/packages/openapi-fetch/test/transform/schemas/transform.yaml @@ -0,0 +1,96 @@ +openapi: 3.0.0 +info: + title: Transform Test API + version: 1.0.0 +paths: + /posts: + get: + summary: Get all posts + responses: + '200': + description: A list of posts + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + type: object + properties: + id: + type: integer + name: + type: string + description: + type: string + sensitive: + type: string + created_at: + type: string + format: date-time + meta: + type: object + properties: + total: + type: integer + post: + summary: Create a new post + requestBody: + content: + application/json: + schema: + type: object + properties: + title: + type: string + content: + type: string + responses: + '200': + description: The created post + content: + application/json: + schema: + type: object + properties: + id: + type: integer + name: + type: string + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + /posts/{id}: + get: + summary: Get a post by ID + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: A post + content: + application/json: + schema: + type: object + properties: + id: + type: integer + title: + type: string + content: + type: string + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time \ No newline at end of file diff --git a/packages/openapi-fetch/test/transform/transform.test.ts b/packages/openapi-fetch/test/transform/transform.test.ts new file mode 100644 index 000000000..cf6b8d2ea --- /dev/null +++ b/packages/openapi-fetch/test/transform/transform.test.ts @@ -0,0 +1,45 @@ +import { assert, expect, test } from "vitest"; +import { createObservedClient } from "../helpers.js"; +import type { paths } from "./schemas/transform.js"; + +interface PostResponse { + id: number; + title: string; + created_at: string | Date; +} + +test("transforms date strings to Date objects", async () => { + const client = createObservedClient( + { + transform: { + response: (method, path, data) => { + if (!data || typeof data !== "object") return data; + + const result = { ...data } as PostResponse; + + if (typeof result.created_at === "string") { + result.created_at = new Date(result.created_at); + } + + return result; + } + } + }, + async () => Response.json({ + id: 1, + title: "Test Post", + created_at: "2023-01-01T00:00:00Z" + }) + ); + + const { data } = await client.GET("/posts/{id}", { + params: { path: { id: 1 } } + }); + + const post = data as PostResponse; + + assert(post.created_at instanceof Date, "created_at should be a Date"); + expect(post.created_at.getFullYear()).toBe(2023); + expect(post.created_at.getMonth()).toBe(0); // January + expect(post.created_at.getDate()).toBe(1); +}); From 0400e5729471ac23704a838ff0085656392efc71 Mon Sep 17 00:00:00 2001 From: Eliya Cohen Date: Mon, 14 Apr 2025 02:22:48 +0300 Subject: [PATCH 2/6] docs: add transform docs --- docs/openapi-fetch/api.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/openapi-fetch/api.md b/docs/openapi-fetch/api.md index 1a504c5d1..d60b9d8ef 100644 --- a/docs/openapi-fetch/api.md +++ b/docs/openapi-fetch/api.md @@ -19,6 +19,7 @@ createClient(options); | `fetch` | `fetch` | Fetch instance used for requests (default: `globalThis.fetch`) | | `querySerializer` | QuerySerializer | (optional) Provide a [querySerializer](#queryserializer) | | `bodySerializer` | BodySerializer | (optional) Provide a [bodySerializer](#bodyserializer) | +| `transform` | TransformOptions| (optional) Provide [transform functions](#transform) for response data | | (Fetch options) | | Any valid fetch option (`headers`, `mode`, `cache`, `signal` …) ([docs](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options) | ## Fetch options @@ -192,6 +193,24 @@ or when instantiating the client. ::: +## transform + +The transform option lets you modify request and response data before it's sent or after it's received. This is useful for tasks like deserialization. + +```ts +const client = createClient({ + transform: { + response: (method, path, data) => { + // Convert date strings to Date objects + if (data?.created_at) { + data.created_at = new Date(data.created_at); + } + return data; + } + } +}); +``` + ## Path serialization openapi-fetch supports path serialization as [outlined in the 3.1 spec](https://swagger.io/docs/specification/serialization/#path). This happens automatically, based on the specific format in your OpenAPI schema: From ab55b09e1e9bfc58cd0400da3e68f7a312c8fc25 Mon Sep 17 00:00:00 2001 From: Eliya Cohen Date: Mon, 14 Apr 2025 02:27:49 +0300 Subject: [PATCH 3/6] fix(transform.test): clean up whitespace and improve formatting in test case --- .../test/transform/transform.test.ts | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/openapi-fetch/test/transform/transform.test.ts b/packages/openapi-fetch/test/transform/transform.test.ts index cf6b8d2ea..b08b8b58f 100644 --- a/packages/openapi-fetch/test/transform/transform.test.ts +++ b/packages/openapi-fetch/test/transform/transform.test.ts @@ -14,26 +14,27 @@ test("transforms date strings to Date objects", async () => { transform: { response: (method, path, data) => { if (!data || typeof data !== "object") return data; - + const result = { ...data } as PostResponse; - + if (typeof result.created_at === "string") { result.created_at = new Date(result.created_at); } - + return result; - } - } + }, + }, }, - async () => Response.json({ - id: 1, - title: "Test Post", - created_at: "2023-01-01T00:00:00Z" - }) + async () => + Response.json({ + id: 1, + title: "Test Post", + created_at: "2023-01-01T00:00:00Z", + }), ); const { data } = await client.GET("/posts/{id}", { - params: { path: { id: 1 } } + params: { path: { id: 1 } }, }); const post = data as PostResponse; From 973e7a92f3cc7d1ce3dc4921bff338f124fcee59 Mon Sep 17 00:00:00 2001 From: Eliya Cohen Date: Mon, 14 Apr 2025 10:22:44 +0300 Subject: [PATCH 4/6] fix(transform.test): lint --- packages/openapi-fetch/test/transform/transform.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/openapi-fetch/test/transform/transform.test.ts b/packages/openapi-fetch/test/transform/transform.test.ts index b08b8b58f..354ced4f8 100644 --- a/packages/openapi-fetch/test/transform/transform.test.ts +++ b/packages/openapi-fetch/test/transform/transform.test.ts @@ -13,7 +13,9 @@ test("transforms date strings to Date objects", async () => { { transform: { response: (method, path, data) => { - if (!data || typeof data !== "object") return data; + if (!data || typeof data !== "object") { + return data + }; const result = { ...data } as PostResponse; From 82465d61e26ce3f508c9f63f70bc98aa5a8205da Mon Sep 17 00:00:00 2001 From: Eliya Cohen Date: Mon, 14 Apr 2025 10:24:28 +0300 Subject: [PATCH 5/6] chore(changeset): add changeset for openapi-fetch transform option --- .changeset/strong-wombats-rhyme.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/strong-wombats-rhyme.md diff --git a/.changeset/strong-wombats-rhyme.md b/.changeset/strong-wombats-rhyme.md new file mode 100644 index 000000000..c09f3bfc9 --- /dev/null +++ b/.changeset/strong-wombats-rhyme.md @@ -0,0 +1,5 @@ +--- +"openapi-fetch": patch +--- + +openapi-fetch - add `transform` option (createClient) for response data handling From 9321c34300f827c4f275eb7828fa31c8c7640349 Mon Sep 17 00:00:00 2001 From: Eliya Cohen Date: Thu, 17 Apr 2025 13:47:17 +0300 Subject: [PATCH 6/6] fix(transform.test): correct linting issues by fixing whitespace in response transformation --- packages/openapi-fetch/test/transform/transform.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/openapi-fetch/test/transform/transform.test.ts b/packages/openapi-fetch/test/transform/transform.test.ts index 354ced4f8..183a632fe 100644 --- a/packages/openapi-fetch/test/transform/transform.test.ts +++ b/packages/openapi-fetch/test/transform/transform.test.ts @@ -14,8 +14,8 @@ test("transforms date strings to Date objects", async () => { transform: { response: (method, path, data) => { if (!data || typeof data !== "object") { - return data - }; + return data; + } const result = { ...data } as PostResponse;