From 91fc3769eca4874085345d2845c115d06bf5232e Mon Sep 17 00:00:00 2001 From: Drew Powers Date: Tue, 27 Jun 2023 23:47:12 -0700 Subject: [PATCH 1/2] Add multipart requestBody support, update docs --- .changeset/long-moose-scream.md | 5 + .changeset/wise-adults-deny.md | 5 + docs/public/assets/openapi-schema.png | Bin 14019 -> 33300 bytes docs/scripts/update-contributors.js | 2 +- docs/src/content/docs/openapi-fetch/api.md | 77 +++++--- docs/src/content/docs/openapi-fetch/index.md | 23 ++- docs/src/data/contributors.json | 2 +- packages/openapi-fetch/README.md | 186 ++++++------------- packages/openapi-fetch/package.json | 2 +- packages/openapi-fetch/src/index.test.ts | 45 +++-- packages/openapi-fetch/src/index.ts | 62 ++++--- packages/openapi-fetch/test/v1.d.ts | 23 +++ packages/openapi-fetch/test/v1.yaml | 33 +++- 13 files changed, 261 insertions(+), 204 deletions(-) create mode 100644 .changeset/long-moose-scream.md create mode 100644 .changeset/wise-adults-deny.md diff --git a/.changeset/long-moose-scream.md b/.changeset/long-moose-scream.md new file mode 100644 index 000000000..5ed1d566d --- /dev/null +++ b/.changeset/long-moose-scream.md @@ -0,0 +1,5 @@ +--- +"openapi-fetch": minor +--- + +Add multipart/form-data request body support diff --git a/.changeset/wise-adults-deny.md b/.changeset/wise-adults-deny.md new file mode 100644 index 000000000..28cb5d647 --- /dev/null +++ b/.changeset/wise-adults-deny.md @@ -0,0 +1,5 @@ +--- +"openapi-fetch": minor +--- + +Breaking: openapi-fetch now just takes the first media type it finds rather than preferring JSON. This is because in the case of `multipart/form-data` vs `application/json`, it’s not inherently clear which you’d want. Or if there were multiple JSON-like media types. diff --git a/docs/public/assets/openapi-schema.png b/docs/public/assets/openapi-schema.png index 0d24c8d2b30d01a4c1405271f0ff87ef1f1d3ca8..b9abd381944eff3fc948399432eb04daa81c91f9 100644 GIT binary patch literal 33300 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 h)4VUoA ({ username, avatar: await fetchAvatar(username) }))); diff --git a/docs/src/content/docs/openapi-fetch/api.md b/docs/src/content/docs/openapi-fetch/api.md index 445f3afef..ef9665a65 100644 --- a/docs/src/content/docs/openapi-fetch/api.md +++ b/docs/src/content/docs/openapi-fetch/api.md @@ -11,11 +11,13 @@ description: openapi-fetch API createClient(options); ``` -| Name | Type | Description | -| :-------------- | :------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `baseUrl` | `string` | Prefix all fetch URLs with this option (e.g. `"https://myapi.dev/v1/"`). | -| `fetch` | `fetch` | Fetch function used for requests (defaults to `globalThis.fetch`) | -| (Fetch options) | | Any valid fetch option (`headers`, `mode`, `cache`, `signal` …) (docs) | +| Name | Type | Description | +| :---------------- | :-------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `baseUrl` | `string` | Prefix all fetch URLs with this option (e.g. `"https://myapi.dev/v1/"`). | +| `fetch` | `fetch` | Fetch function used for requests (defaults to `globalThis.fetch`) | +| `querySerializer` | QuerySerializer | (optional) Serialize query params for all requests (default: `new URLSearchParams()`) | +| `bodySerializer` | BodySerializer | (optional) Serialize request body object for all requests (default: `JSON.stringify()`) | +| (Fetch options) | | Any valid fetch option (`headers`, `mode`, `cache`, `signal` …) (docs) | ## Fetch options @@ -25,35 +27,56 @@ The following options apply to all request methods (`.get()`, `.post()`, etc.) client.get("/my-url", options); ``` -| Name | Type | Description | -| :---------------- | :---------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `params` | ParamsObject | Provide `path` and `query` params from the OpenAPI schema | -| `params.path` | `{ [name]: value }` | Provide all `path` params (params that are part of the URL) | -| `params.query` | `{ [name]: value }` | Provide all `query params (params that are part of the searchParams | -| `body` | `{ [name]:value }` | The requestBody data, if needed (PUT/POST/PATCH/DEL only) | -| `querySerializer` | QuerySerializer | (optional) Override default param serialization (see [Parameter Serialization](#parameter-serialization)) | -| `parseAs` | `"json"` \| `"text"` \| `"arrayBuffer"` \| `"blob"` \| `"stream"` | Decide how to parse the response body, with `"stream"` skipping processing altogether (default: `"json"`) | -| (Fetch options) | | Any valid fetch option (`headers`, `mode`, `cache`, `signal` …) (docs) | +| Name | Type | Description | +| :---------------- | :---------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `params` | ParamsObject | Provide `path` and `query` params from the OpenAPI schema | +| `params.path` | `{ [name]: value }` | Provide all `path` params (params that are part of the URL) | +| `params.query` | `{ [name]: value }` | Provide all `query params (params that are part of the searchParams | +| `body` | `{ [name]:value }` | The requestBody data, if needed (PUT/POST/PATCH/DEL only) | +| `querySerializer` | QuerySerializer | (optional) Serialize query params for this request only (default: `new URLSearchParams()`) | +| `bodySerializer` | BodySerializer | (optional) Serialize request body for this request only (default: `JSON.stringify()`) | +| `parseAs` | `"json"` \| `"text"` \| `"arrayBuffer"` \| `"blob"` \| `"stream"` | Parse the response body, with `"stream"` skipping processing altogether (default: `"json"`) | +| (Fetch options) | | Any valid fetch option (`headers`, `mode`, `cache`, `signal` …) (docs) | -### Parameter Serialization +### querySerializer -In the spirit of being lightweight, this library only uses URLSearchParams to serialize parameters. So for complex query param types (e.g. arrays) you’ll need to provide your own `querySerializer()` method that transforms query params into a URL-safe string: +This library uses URLSearchParams to serialize query parameters. For complex query param types (e.g. arrays) you’ll need to provide your own `querySerializer()` method that transforms query params into a URL-safe string: ```ts -import createClient from "openapi-fetch"; -import { paths } from "./v1"; // generated from openapi-typescript - -const { get, post } = createClient({ baseUrl: "https://myapi.dev/v1/" }); - -const { data, error } = await get("/blogposts/{post_id}", { +const { data, error } = await get("/search", { params: { - path: { post_id: "my-post" }, - query: { version: 2 }, + query: { tags: ["food", "california", "healthy"] }, + }, + querySerializer(q) { + let s = ""; + for (const [k, v] of Object.entries(q)) { + if (Array.isArray(v)) { + s += `${k}[]=${v.join(",")}`; + } else { + s += `${k}=${v}`; + } + } + return s; // ?tags[]=food&tags[]=california&tags[]=healthy }, - querySerializer: (q) => `v=${q.version}`, // ✅ Still typechecked based on the URL! }); ``` -Note that this happens **at the request level** so that you still get correct type inference for that URL’s specific query params. +### bodySerializer + +Similar to [querySerializer](#querySerializer), bodySerializer works for requestBody. You probably only need this when using `multipart/form-data`: -_Thanks, [@ezpuzz](https://github.com/ezpuzz)!_ +```ts +const { data, error } = await put("/submit", { + body: { + name: "", + query: { version: 2 }, + }, + bodySerializer(body) { + const fd = new FormData(); + for (const [k, v] of Object.entries(body)) { + fd.append(k, v); + } + return fd; + }, +}); +``` diff --git a/docs/src/content/docs/openapi-fetch/index.md b/docs/src/content/docs/openapi-fetch/index.md index 7b44d0cac..2cbd0055e 100644 --- a/docs/src/content/docs/openapi-fetch/index.md +++ b/docs/src/content/docs/openapi-fetch/index.md @@ -5,7 +5,7 @@ description: Get Started with openapi-fetch openapi-fetch -openapi-fetch is an ultra-fast fetch client for TypeScript using your OpenAPI schema. Weighs in at **1 kb** and has virtually zero runtime. Works with React, Vue, Svelte, or vanilla JS. +openapi-fetch applies your OpenAPI types to the native fetch API via TypeScript. Weighs in at **1 kb** and has virtually zero runtime. Works with React, Vue, Svelte, or vanilla JS. | Library | Size (min) | | :----------------------------- | ---------: | @@ -19,7 +19,7 @@ The syntax is inspired by popular libraries like react-query or Apollo client, b import createClient from "openapi-fetch"; import { paths } from "./v1"; // generated from openapi-typescript -const { get, post } = createClient({ baseUrl: "https://myapi.dev/v1/" }); +const { get, put } = createClient({ baseUrl: "https://myapi.dev/v1/" }); // Type-checked request await put("/blogposts", { @@ -30,8 +30,7 @@ await put("/blogposts", { }); // Type-checked response -const { data, error } = await get("/blogposts/my-blog-post"); - +const { data, error } = await get("/blogposts/{post_id}", { params: { path: { post_id: "123" } } }); console.log(data.title); // ❌ 'data' is possibly 'undefined' console.log(error.message); // ❌ 'error' is possibly 'undefined' console.log(data?.foo); // ❌ Property 'foo' does not exist on type … @@ -81,17 +80,17 @@ And run `npm run test:ts` in your CI to catch type errors. ## Usage -Using **openapi-fetch** is as easy as reading your schema! For example, given the following schema: +Using **openapi-fetch** is as easy as reading your schema: ![OpenAPI schema example](/assets/openapi-schema.png) -Here’s how you’d fetch GET `/blogposts/{post_id}` and POST `/blogposts`: +Here’s how you’d fetch GET `/blogposts/{post_id}` and PUT `/blogposts`: ```ts import createClient from "openapi-fetch"; import { paths } from "./v1"; -const { get, post } = createClient({ baseUrl: "https://myapi.dev/v1/" }); +const { get, put } = createClient({ baseUrl: "https://myapi.dev/v1/" }); const { data, error } = await get("/blogposts/{post_id}", { params: { @@ -100,7 +99,7 @@ const { data, error } = await get("/blogposts/{post_id}", { }, }); -const { data, error } = await post("/blogposts", { +const { data, error } = await put("/blogposts", { body: { title: "New Post", body: "

New post body

", @@ -134,3 +133,11 @@ All methods return an object with **data**, **error**, and **response**. - **error** likewise contains that endpoint’s `4xx`/`5xx` response if the server returned either; otherwise it will be `undefined` - _Note: `default` will also be interpreted as `error`, since its intent is handling unexpected HTTP codes_ - **response** has response info like `status`, `headers`, etc. It is not typechecked. + +## Version Support + +openapi-fetch implements the [native fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) which is available in all major browsers. + +If using in a Node.js environment, version 18 or greater is recommended (newer is better). + +TypeScript support is pretty far-reaching as this library doesn’t use any cutting-edge features, but using the latest version of TypeScript is always recommended for accuracy. diff --git a/docs/src/data/contributors.json b/docs/src/data/contributors.json index ea2fbd996..29f02f19b 100644 --- a/docs/src/data/contributors.json +++ b/docs/src/data/contributors.json @@ -1 +1 @@ -{"openapi-typescript":[{"username":"drwpow","avatar":"https://avatars.githubusercontent.com/u/1369770?v=4?s=400"},{"username":"psmyrdek","avatar":"https://avatars.githubusercontent.com/u/6187417?v=4?s=400"},{"username":"enmand","avatar":"https://avatars.githubusercontent.com/u/432487?v=4?s=400"},{"username":"atlefren","avatar":"https://avatars.githubusercontent.com/u/1829927?v=4?s=400"},{"username":"tpdewolf","avatar":"https://avatars.githubusercontent.com/u/4455209?v=4?s=400"},{"username":"tombarton","avatar":"https://avatars.githubusercontent.com/u/6222711?v=4?s=400"},{"username":"svnv","avatar":"https://avatars.githubusercontent.com/u/1080888?v=4?s=400"},{"username":"sorin-davidoi","avatar":"https://avatars.githubusercontent.com/u/2109702?v=4?s=400"},{"username":"scvnathan","avatar":"https://avatars.githubusercontent.com/u/73474?v=4?s=400"},{"username":"lbenie","avatar":"https://avatars.githubusercontent.com/u/7316046?v=4?s=400"},{"username":"bokub","avatar":"https://avatars.githubusercontent.com/u/17952318?v=4?s=400"},{"username":"antonk52","avatar":"https://avatars.githubusercontent.com/u/5817809?v=4?s=400"},{"username":"tshelburne","avatar":"https://avatars.githubusercontent.com/u/1202267?v=4?s=400"},{"username":"mmiszy","avatar":"https://avatars.githubusercontent.com/u/1338731?v=4?s=400"},{"username":"skh-","avatar":"https://avatars.githubusercontent.com/u/1292598?v=4?s=400"},{"username":"BlooJeans","avatar":"https://avatars.githubusercontent.com/u/1751182?v=4?s=400"},{"username":"selbekk","avatar":"https://avatars.githubusercontent.com/u/1307267?v=4?s=400"},{"username":"Mause","avatar":"https://avatars.githubusercontent.com/u/1405026?v=4?s=400"},{"username":"henhal","avatar":"https://avatars.githubusercontent.com/u/9608258?v=4?s=400"},{"username":"gr2m","avatar":"https://avatars.githubusercontent.com/u/39992?v=4?s=400"},{"username":"samdbmg","avatar":"https://avatars.githubusercontent.com/u/408983?v=4?s=400"},{"username":"rendall","avatar":"https://avatars.githubusercontent.com/u/293263?v=4?s=400"},{"username":"robertmassaioli","avatar":"https://avatars.githubusercontent.com/u/149178?v=4?s=400"},{"username":"jankuca","avatar":"https://avatars.githubusercontent.com/u/367262?v=4?s=400"},{"username":"th-m","avatar":"https://avatars.githubusercontent.com/u/13792029?v=4?s=400"},{"username":"asithade","avatar":"https://avatars.githubusercontent.com/u/3814354?v=4?s=400"},{"username":"MikeYermolayev","avatar":"https://avatars.githubusercontent.com/u/8783498?v=4?s=400"},{"username":"radist2s","avatar":"https://avatars.githubusercontent.com/u/725645?v=4?s=400"},{"username":"FedeBev","avatar":"https://avatars.githubusercontent.com/u/22151395?v=4?s=400"},{"username":"yamacent","avatar":"https://avatars.githubusercontent.com/u/8544439?v=4?s=400"},{"username":"dnalborczyk","avatar":"https://avatars.githubusercontent.com/u/2903325?v=4?s=400"},{"username":"FabioWanner","avatar":"https://avatars.githubusercontent.com/u/46821078?v=4?s=400"},{"username":"ashsmith","avatar":"https://avatars.githubusercontent.com/u/1086841?v=4?s=400"},{"username":"mehalter","avatar":"https://avatars.githubusercontent.com/u/1591837?v=4?s=400"},{"username":"Chrg1001","avatar":"https://avatars.githubusercontent.com/u/40189653?v=4?s=400"},{"username":"sharmarajdaksh","avatar":"https://avatars.githubusercontent.com/u/33689528?v=4?s=400"},{"username":"shuluster","avatar":"https://avatars.githubusercontent.com/u/1707910?v=4?s=400"},{"username":"FDiskas","avatar":"https://avatars.githubusercontent.com/u/468006?v=4?s=400"},{"username":"ericzorn93","avatar":"https://avatars.githubusercontent.com/u/22532542?v=4?s=400"},{"username":"mbelsky","avatar":"https://avatars.githubusercontent.com/u/3923527?v=4?s=400"},{"username":"Peteck","avatar":"https://avatars.githubusercontent.com/u/129566390?v=4?s=400"},{"username":"rustyconover","avatar":"https://avatars.githubusercontent.com/u/731941?v=4?s=400"},{"username":"bunkscene","avatar":"https://avatars.githubusercontent.com/u/2693678?v=4?s=400"},{"username":"ottomated","avatar":"https://avatars.githubusercontent.com/u/31470743?v=4?s=400"},{"username":"sadfsdfdsa","avatar":"https://avatars.githubusercontent.com/u/28733669?v=4?s=400"},{"username":"ajaishankar","avatar":"https://avatars.githubusercontent.com/u/328008?v=4?s=400"},{"username":"dominikdosoudil","avatar":"https://avatars.githubusercontent.com/u/15929942?v=4?s=400"},{"username":"kgtkr","avatar":"https://avatars.githubusercontent.com/u/17868838?v=4?s=400"},{"username":"berzi","avatar":"https://avatars.githubusercontent.com/u/32619123?v=4?s=400"},{"username":"PhilipTrauner","avatar":"https://avatars.githubusercontent.com/u/9287847?v=4?s=400"},{"username":"Powell-v2","avatar":"https://avatars.githubusercontent.com/u/25308326?v=4?s=400"},{"username":"duncanbeevers","avatar":"https://avatars.githubusercontent.com/u/7367?v=4?s=400"},{"username":"tkukushkin","avatar":"https://avatars.githubusercontent.com/u/1482516?v=4?s=400"},{"username":"Semigradsky","avatar":"https://avatars.githubusercontent.com/u/1198848?v=4?s=400"},{"username":"MrLeebo","avatar":"https://avatars.githubusercontent.com/u/2754163?v=4?s=400"},{"username":"axelhzf","avatar":"https://avatars.githubusercontent.com/u/175627?v=4?s=400"},{"username":"imagoiq","avatar":"https://avatars.githubusercontent.com/u/12294151?v=4?s=400"},{"username":"BTMPL","avatar":"https://avatars.githubusercontent.com/u/247153?v=4?s=400"},{"username":"HiiiiD","avatar":"https://avatars.githubusercontent.com/u/61231210?v=4?s=400"},{"username":"yacinehmito","avatar":"https://avatars.githubusercontent.com/u/6893840?v=4?s=400"},{"username":"sajadtorkamani","avatar":"https://avatars.githubusercontent.com/u/9380313?v=4?s=400"},{"username":"mvdbeek","avatar":"https://avatars.githubusercontent.com/u/6804901?v=4?s=400"},{"username":"sgrimm","avatar":"https://avatars.githubusercontent.com/u/1248649?v=4?s=400"},{"username":"Swiftwork","avatar":"https://avatars.githubusercontent.com/u/455178?v=4?s=400"},{"username":"mtth","avatar":"https://avatars.githubusercontent.com/u/1216372?v=4?s=400"},{"username":"mitchell-merry","avatar":"https://avatars.githubusercontent.com/u/8567231?v=4?s=400"},{"username":"qnp","avatar":"https://avatars.githubusercontent.com/u/6012554?v=4?s=400"},{"username":"shoffmeister","avatar":"https://avatars.githubusercontent.com/u/3868036?v=4?s=400"},{"username":"liangskyli","avatar":"https://avatars.githubusercontent.com/u/31531283?v=4?s=400"},{"username":"happycollision","avatar":"https://avatars.githubusercontent.com/u/3663628?v=4?s=400"},{"username":"barakalon","avatar":"https://avatars.githubusercontent.com/u/12398927?v=4?s=400"},{"username":"pvanagtmaal","avatar":"https://avatars.githubusercontent.com/u/5946464?v=4?s=400"}],"openapi-fetch":[{"username":"drwpow","avatar":"https://avatars.githubusercontent.com/u/1369770?v=4?s=400"},{"username":"fergusean","avatar":"https://avatars.githubusercontent.com/u/1029297?v=4?s=400"},{"username":"shinzui","avatar":"https://avatars.githubusercontent.com/u/519?v=4?s=400"},{"username":"ezpuzz","avatar":"https://avatars.githubusercontent.com/u/672182?v=4?s=400"},{"username":"KotoriK","avatar":"https://avatars.githubusercontent.com/u/52659125?v=4?s=400"},{"username":"fletchertyler914","avatar":"https://avatars.githubusercontent.com/u/3344498?v=4?s=400"},{"username":"nholik","avatar":"https://avatars.githubusercontent.com/u/2022214?v=4?s=400"},{"username":"roj1512","avatar":"https://avatars.githubusercontent.com/u/49933115?v=4?s=400"},{"username":"nickcaballero","avatar":"https://avatars.githubusercontent.com/u/355976?v=4?s=400"},{"username":"hd-o","avatar":"https://avatars.githubusercontent.com/u/58871222?v=4?s=400"},{"username":"kecrily","avatar":"https://avatars.githubusercontent.com/u/45708948?v=4?s=400"}]} \ No newline at end of file +{"openapi-typescript":[{"username":"drwpow","avatar":"https://avatars.githubusercontent.com/u/1369770?v=4?s=400"},{"username":"psmyrdek","avatar":"https://avatars.githubusercontent.com/u/6187417?v=4?s=400"},{"username":"enmand","avatar":"https://avatars.githubusercontent.com/u/432487?v=4?s=400"},{"username":"atlefren","avatar":"https://avatars.githubusercontent.com/u/1829927?v=4?s=400"},{"username":"tpdewolf","avatar":"https://avatars.githubusercontent.com/u/4455209?v=4?s=400"},{"username":"tombarton","avatar":"https://avatars.githubusercontent.com/u/6222711?v=4?s=400"},{"username":"svnv","avatar":"https://avatars.githubusercontent.com/u/1080888?v=4?s=400"},{"username":"sorin-davidoi","avatar":"https://avatars.githubusercontent.com/u/2109702?v=4?s=400"},{"username":"scvnathan","avatar":"https://avatars.githubusercontent.com/u/73474?v=4?s=400"},{"username":"lbenie","avatar":"https://avatars.githubusercontent.com/u/7316046?v=4?s=400"},{"username":"bokub","avatar":"https://avatars.githubusercontent.com/u/17952318?v=4?s=400"},{"username":"antonk52","avatar":"https://avatars.githubusercontent.com/u/5817809?v=4?s=400"},{"username":"tshelburne","avatar":"https://avatars.githubusercontent.com/u/1202267?v=4?s=400"},{"username":"mmiszy","avatar":"https://avatars.githubusercontent.com/u/1338731?v=4?s=400"},{"username":"skh-","avatar":"https://avatars.githubusercontent.com/u/1292598?v=4?s=400"},{"username":"BlooJeans","avatar":"https://avatars.githubusercontent.com/u/1751182?v=4?s=400"},{"username":"selbekk","avatar":"https://avatars.githubusercontent.com/u/1307267?v=4?s=400"},{"username":"Mause","avatar":"https://avatars.githubusercontent.com/u/1405026?v=4?s=400"},{"username":"henhal","avatar":"https://avatars.githubusercontent.com/u/9608258?v=4?s=400"},{"username":"gr2m","avatar":"https://avatars.githubusercontent.com/u/39992?v=4?s=400"},{"username":"samdbmg","avatar":"https://avatars.githubusercontent.com/u/408983?v=4?s=400"},{"username":"rendall","avatar":"https://avatars.githubusercontent.com/u/293263?v=4?s=400"},{"username":"robertmassaioli","avatar":"https://avatars.githubusercontent.com/u/149178?v=4?s=400"},{"username":"jankuca","avatar":"https://avatars.githubusercontent.com/u/367262?v=4?s=400"},{"username":"th-m","avatar":"https://avatars.githubusercontent.com/u/13792029?v=4?s=400"},{"username":"asithade","avatar":"https://avatars.githubusercontent.com/u/3814354?v=4?s=400"},{"username":"MikeYermolayev","avatar":"https://avatars.githubusercontent.com/u/8783498?v=4?s=400"},{"username":"radist2s","avatar":"https://avatars.githubusercontent.com/u/725645?v=4?s=400"},{"username":"FedeBev","avatar":"https://avatars.githubusercontent.com/u/22151395?v=4?s=400"},{"username":"yamacent","avatar":"https://avatars.githubusercontent.com/u/8544439?v=4?s=400"},{"username":"dnalborczyk","avatar":"https://avatars.githubusercontent.com/u/2903325?v=4?s=400"},{"username":"FabioWanner","avatar":"https://avatars.githubusercontent.com/u/46821078?v=4?s=400"},{"username":"ashsmith","avatar":"https://avatars.githubusercontent.com/u/1086841?v=4?s=400"},{"username":"mehalter","avatar":"https://avatars.githubusercontent.com/u/1591837?v=4?s=400"},{"username":"Chrg1001","avatar":"https://avatars.githubusercontent.com/u/40189653?v=4?s=400"},{"username":"sharmarajdaksh","avatar":"https://avatars.githubusercontent.com/u/33689528?v=4?s=400"},{"username":"shuluster","avatar":"https://avatars.githubusercontent.com/u/1707910?v=4?s=400"},{"username":"FDiskas","avatar":"https://avatars.githubusercontent.com/u/468006?v=4?s=400"},{"username":"ericzorn93","avatar":"https://avatars.githubusercontent.com/u/22532542?v=4?s=400"},{"username":"mbelsky","avatar":"https://avatars.githubusercontent.com/u/3923527?v=4?s=400"},{"username":"Peteck","avatar":"https://avatars.githubusercontent.com/u/129566390?v=4?s=400"},{"username":"rustyconover","avatar":"https://avatars.githubusercontent.com/u/731941?v=4?s=400"},{"username":"bunkscene","avatar":"https://avatars.githubusercontent.com/u/2693678?v=4?s=400"},{"username":"ottomated","avatar":"https://avatars.githubusercontent.com/u/31470743?v=4?s=400"},{"username":"sadfsdfdsa","avatar":"https://avatars.githubusercontent.com/u/28733669?v=4?s=400"},{"username":"ajaishankar","avatar":"https://avatars.githubusercontent.com/u/328008?v=4?s=400"},{"username":"dominikdosoudil","avatar":"https://avatars.githubusercontent.com/u/15929942?v=4?s=400"},{"username":"kgtkr","avatar":"https://avatars.githubusercontent.com/u/17868838?v=4?s=400"},{"username":"berzi","avatar":"https://avatars.githubusercontent.com/u/32619123?v=4?s=400"},{"username":"PhilipTrauner","avatar":"https://avatars.githubusercontent.com/u/9287847?v=4?s=400"},{"username":"Powell-v2","avatar":"https://avatars.githubusercontent.com/u/25308326?v=4?s=400"},{"username":"duncanbeevers","avatar":"https://avatars.githubusercontent.com/u/7367?v=4?s=400"},{"username":"tkukushkin","avatar":"https://avatars.githubusercontent.com/u/1482516?v=4?s=400"},{"username":"Semigradsky","avatar":"https://avatars.githubusercontent.com/u/1198848?v=4?s=400"},{"username":"MrLeebo","avatar":"https://avatars.githubusercontent.com/u/2754163?v=4?s=400"},{"username":"axelhzf","avatar":"https://avatars.githubusercontent.com/u/175627?v=4?s=400"},{"username":"imagoiq","avatar":"https://avatars.githubusercontent.com/u/12294151?v=4?s=400"},{"username":"BTMPL","avatar":"https://avatars.githubusercontent.com/u/247153?v=4?s=400"},{"username":"HiiiiD","avatar":"https://avatars.githubusercontent.com/u/61231210?v=4?s=400"},{"username":"yacinehmito","avatar":"https://avatars.githubusercontent.com/u/6893840?v=4?s=400"},{"username":"sajadtorkamani","avatar":"https://avatars.githubusercontent.com/u/9380313?v=4?s=400"},{"username":"mvdbeek","avatar":"https://avatars.githubusercontent.com/u/6804901?v=4?s=400"},{"username":"sgrimm","avatar":"https://avatars.githubusercontent.com/u/1248649?v=4?s=400"},{"username":"Swiftwork","avatar":"https://avatars.githubusercontent.com/u/455178?v=4?s=400"},{"username":"mtth","avatar":"https://avatars.githubusercontent.com/u/1216372?v=4?s=400"},{"username":"mitchell-merry","avatar":"https://avatars.githubusercontent.com/u/8567231?v=4?s=400"},{"username":"qnp","avatar":"https://avatars.githubusercontent.com/u/6012554?v=4?s=400"},{"username":"shoffmeister","avatar":"https://avatars.githubusercontent.com/u/3868036?v=4?s=400"},{"username":"liangskyli","avatar":"https://avatars.githubusercontent.com/u/31531283?v=4?s=400"},{"username":"happycollision","avatar":"https://avatars.githubusercontent.com/u/3663628?v=4?s=400"},{"username":"barakalon","avatar":"https://avatars.githubusercontent.com/u/12398927?v=4?s=400"},{"username":"pvanagtmaal","avatar":"https://avatars.githubusercontent.com/u/5946464?v=4?s=400"}],"openapi-fetch":[{"username":"drwpow","avatar":"https://avatars.githubusercontent.com/u/1369770?v=4?s=400"},{"username":"fergusean","avatar":"https://avatars.githubusercontent.com/u/1029297?v=4?s=400"},{"username":"shinzui","avatar":"https://avatars.githubusercontent.com/u/519?v=4?s=400"},{"username":"ezpuzz","avatar":"https://avatars.githubusercontent.com/u/672182?v=4?s=400"},{"username":"KotoriK","avatar":"https://avatars.githubusercontent.com/u/52659125?v=4?s=400"},{"username":"fletchertyler914","avatar":"https://avatars.githubusercontent.com/u/3344498?v=4?s=400"},{"username":"nholik","avatar":"https://avatars.githubusercontent.com/u/2022214?v=4?s=400"},{"username":"roj1512","avatar":"https://avatars.githubusercontent.com/u/49933115?v=4?s=400"},{"username":"nickcaballero","avatar":"https://avatars.githubusercontent.com/u/355976?v=4?s=400"},{"username":"hd-o","avatar":"https://avatars.githubusercontent.com/u/58871222?v=4?s=400"},{"username":"kecrily","avatar":"https://avatars.githubusercontent.com/u/45708948?v=4?s=400"},{"username":"psychedelicious","avatar":"https://avatars.githubusercontent.com/u/4822129?v=4?s=400"}]} \ No newline at end of file diff --git a/packages/openapi-fetch/README.md b/packages/openapi-fetch/README.md index a90dce5d8..31056ae1e 100644 --- a/packages/openapi-fetch/README.md +++ b/packages/openapi-fetch/README.md @@ -1,6 +1,6 @@ openapi-fetch -openapi-fetch is an ultra-fast fetch client for TypeScript using your OpenAPI schema. Weighs in at **1 kb** and has virtually zero runtime. Works with React, Vue, Svelte, or vanilla JS. +openapi-fetch applies your OpenAPI types to the native fetch API via TypeScript. Weighs in at **1 kb** and has virtually zero runtime. Works with React, Vue, Svelte, or vanilla JS. | Library | Size (min) | | :----------------------------- | ---------: | @@ -76,17 +76,17 @@ And run `npm run test:ts` in your CI to catch type errors. ## 🏓 Usage -Using **openapi-fetch** is as easy as reading your schema! For example, given the following schema: +Using **openapi-fetch** is as easy as reading your schema: -![OpenAPI schema example](../../../docs/public/assets/openapi-schema.png) +![OpenAPI schema example](../../docs/public/assets/openapi-schema.png) -Here’s how you’d fetch GET `/blogpost/{post_id}` and POST `/blogposts`: +Here’s how you’d fetch GET `/blogposts/{post_id}` and PUT `/blogposts`: ```ts import createClient from "openapi-fetch"; import { paths } from "./v1"; -const { get, post } = createClient({ baseUrl: "https://myapi.dev/v1/" }); +const { get, put } = createClient({ baseUrl: "https://myapi.dev/v1/" }); const { data, error } = await get("/blogposts/{post_id}", { params: { @@ -95,7 +95,7 @@ const { data, error } = await get("/blogposts/{post_id}", { }, }); -const { data, error } = await post("/blogposts", { +const { data, error } = await put("/blogposts", { body: { title: "New Post", body: "

New post body

", @@ -130,6 +130,14 @@ All methods return an object with **data**, **error**, and **response**. - _Note: `default` will also be interpreted as `error`, since its intent is handling unexpected HTTP codes_ - **response** has response info like `status`, `headers`, etc. It is not typechecked. +## Version Support + +openapi-fetch implements the [native fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) which is available in all major browsers. + +If using in a Node.js environment, version 18 or greater is recommended (newer is better). + +TypeScript support is pretty far-reaching as this library doesn’t use any cutting-edge features, but using the latest version of TypeScript is always recommended for accuracy. + ## API ### Create Client @@ -140,147 +148,73 @@ All methods return an object with **data**, **error**, and **response**. createClient(options); ``` -| Name | Type | Description | -| :-------------- | :------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `baseUrl` | `string` | Prefix all fetch URLs with this option (e.g. `"https://myapi.dev/v1/"`). | -| `fetch` | `fetch` | Fetch function used for requests (defaults to `globalThis.fetch`) | -| (Fetch options) | | Any valid fetch option (`headers`, `mode`, `cache`, `signal` …) (docs) | +| Name | Type | Description | +| :---------------- | :-------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `baseUrl` | `string` | Prefix all fetch URLs with this option (e.g. `"https://myapi.dev/v1/"`). | +| `fetch` | `fetch` | Fetch function used for requests (defaults to `globalThis.fetch`) | +| `querySerializer` | QuerySerializer | (optional) Serialize query params for all requests (default: `new URLSearchParams()`) | +| `bodySerializer` | BodySerializer | (optional) Serialize request body object for all requests (default: `JSON.stringify()`) | +| (Fetch options) | | Any valid fetch option (`headers`, `mode`, `cache`, `signal` …) (docs) | ### Fetch options -```ts -import { paths } from "./v1"; - -const { get, put, post, del, options, head, patch, trace } = createClient({ baseUrl: "https://myapi.dev/v1/" }); - -const { data, error, response } = await get("/my-url", options); -``` - -| Name | Type | Description | -| :---------------- | :-----------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `params` | ParamsObject | Provide `path` and `query` params from the OpenAPI schema | -| `params.path` | `{ [name]: value }` | Provide all `path` params (params that are part of the URL) | -| `params.query` | `{ [name]: value }` | Provide all `query params (params that are part of the searchParams | -| `body` | `{ [name]:value }` | The requestBody data, if needed (PUT/POST/PATCH/DEL only) | -| `querySerializer` | QuerySerializer | (optional) Override default param serialization (see [Parameter Serialization](#parameter-serialization)) | -| (Fetch options) | | Any valid fetch option (`headers`, `mode`, `cache`, `signal` …) (docs) | - -#### 🔀 Parameter Serialization - -In the spirit of being lightweight, this library only uses URLSearchParams to serialize parameters. So for complex query param types (e.g. arrays) you’ll need to provide your own `querySerializer()` method that transforms query params into a URL-safe string: +The following options apply to all request methods (`.get()`, `.post()`, etc.) ```ts -import createClient from "openapi-fetch"; -import { paths } from "./v1"; // generated from openapi-typescript - -const { get, post } = createClient({ baseUrl: "https://myapi.dev/v1/" }); - -const { data, error } = await get("/post/{post_id}", { - params: { - path: { post_id: "my-post" }, - query: { version: 2 }, - }, - querySerializer: (q) => `v=${q.version}`, // ✅ Still typechecked based on the URL! -}); +client.get("/my-url", options); ``` -Note that this happens **at the request level** so that you still get correct type inference for that URL’s specific query params. +| Name | Type | Description | +| :---------------- | :---------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `params` | ParamsObject | Provide `path` and `query` params from the OpenAPI schema | +| `params.path` | `{ [name]: value }` | Provide all `path` params (params that are part of the URL) | +| `params.query` | `{ [name]: value }` | Provide all `query params (params that are part of the searchParams | +| `body` | `{ [name]:value }` | The requestBody data, if needed (PUT/POST/PATCH/DEL only) | +| `querySerializer` | QuerySerializer | (optional) Serialize query params for this request only (default: `new URLSearchParams()`) | +| `bodySerializer` | BodySerializer | (optional) Serialize request body for this request only (default: `JSON.stringify()`) | +| `parseAs` | `"json"` \| `"text"` \| `"arrayBuffer"` \| `"blob"` \| `"stream"` | Parse the response body, with `"stream"` skipping processing altogether (default: `"json"`) | +| (Fetch options) | | Any valid fetch option (`headers`, `mode`, `cache`, `signal` …) (docs) | -_Thanks, [@ezpuzz](https://github.com/ezpuzz)!_ +### querySerializer -Provide a `querySerializer()` to `createClient()` to globally override the default `URLSearchParams` serializer. Serializers provided to a specific request method still override the global default. +This library uses URLSearchParams to serialize query parameters. For complex query param types (e.g. arrays) you’ll need to provide your own `querySerializer()` method that transforms query params into a URL-safe string: ```ts -import createClient, { defaultSerializer } from "openapi-fetch"; -import { paths } from "./v1"; // generated from openapi-typescript -import { queryString } from "query-string"; - -const { get, post } = createClient({ - baseUrl: "https://myapi.dev/v1/", - querySerializer: (q) => queryString.stringify(q, { arrayFormat: "none" }), // Override the default `URLSearchParams` serializer -}); - -const { data, error } = await get("/posts/", { +const { data, error } = await get("/search", { params: { - query: { categories: ["dogs", "cats", "lizards"] }, // Use the serializer specified in `createClient()` + query: { tags: ["food", "california", "healthy"] }, }, -}); - -const { data, error } = await get("/images/{image_id}", { - params: { - path: { image_id: "image-id" }, - query: { size: 512 }, + querySerializer(q) { + let s = ""; + for (const [k, v] of Object.entries(q)) { + if (Array.isArray(v)) { + s += `${k}[]=${v.join(",")}`; + } else { + s += `${k}=${v}`; + } + } + return s; // ?tags[]=food&tags[]=california&tags[]=healthy }, - querySerializer: defaultSerializer, // Use `openapi-fetch`'s `URLSearchParams` serializer }); ``` -_Thanks, [@psychedelicious](https://github.com/psychedelicious)!_ - -## Examples - -### 🔒 Handling Auth - -Authentication often requires some reactivity dependent on a token. Since this library is so low-level, there are myriad ways to handle it: +### bodySerializer -#### Nano Stores - -Here’s how it can be handled using [Nano Stores](https://github.com/nanostores/nanostores), a tiny (334 b), universal signals store: +Similar to [querySerializer](#querySerializer), bodySerializer works for requestBody. You probably only need this when using `multipart/form-data`: ```ts -// src/lib/api/index.ts -import { atom, computed } from "nanostores"; -import createClient from "openapi-fetch"; -import { paths } from "./v1"; - -export const authToken = atom(); -someAuthMethod().then((newToken) => authToken.set(newToken)); - -export const client = computed(authToken, (currentToken) => - createClient({ - headers: currentToken ? { Authorization: `Bearer ${currentToken}` } : {}, - baseUrl: "https://myapi.dev/v1/", - }) -); - -// src/some-other-file.ts -import { client } from "./lib/api"; - -const { get, post } = client.get(); - -get("/some-authenticated-url", { - /* … */ -}); -``` - -#### Vanilla JS Proxies - -You can also use [proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) which are now supported in all modern browsers: - -```ts -// src/lib/api/index.ts -import createClient from "openapi-fetch"; -import { paths } from "./v1"; - -let authToken: string | undefined = undefined; -someAuthMethod().then((newToken) => (authToken = newToken)); - -const baseClient = createClient({ baseUrl: "https://myapi.dev/v1/" }); -export default new Proxy(baseClient, { - get(_, key: keyof typeof baseClient) { - const newClient = createClient({ - headers: authToken ? { Authorization: `Bearer ${authToken}` } : {}, - baseUrl: "https://myapi.dev/v1/", - }); - return newClient[key]; +const { data, error } = await put("/submit", { + body: { + name: "", + query: { version: 2 }, + }, + bodySerializer(body) { + const fd = new FormData(); + for (const [k, v] of Object.entries(body)) { + fd.append(k, v); + } + return fd; }, -}); - -// src/some-other-file.ts -import client from "./lib/api"; - -client.get("/some-authenticated-url", { - /* … */ }); ``` diff --git a/packages/openapi-fetch/package.json b/packages/openapi-fetch/package.json index 38803a720..112b0c735 100644 --- a/packages/openapi-fetch/package.json +++ b/packages/openapi-fetch/package.json @@ -1,6 +1,6 @@ { "name": "openapi-fetch", - "description": "Ultra-fast fetching for TypeScript generated automatically from your OpenAPI schema. Weighs in at 1 kb and has virtually zero runtime. Works with React, Vue, Svelte, or vanilla JS.", + "description": "Fetch using your OpenAPI types. Weighs in at 1 kb and has virtually zero runtime. Works with React, Vue, Svelte, or vanilla JS.", "version": "0.5.0", "author": { "name": "Drew Powers", diff --git a/packages/openapi-fetch/src/index.test.ts b/packages/openapi-fetch/src/index.test.ts index 1935a63bf..04e09101b 100644 --- a/packages/openapi-fetch/src/index.test.ts +++ b/packages/openapi-fetch/src/index.test.ts @@ -257,12 +257,43 @@ describe("client", () => { const client = createClient(); mockFetchOnce({ status: 200, body: "{}" }); await client.get("/blogposts/{post_id}", { - params: { path: { post_id: "post?id = 🥴" }, query: {} }, + params: { + path: { + post_id: "post?id = 🥴", + }, + }, }); // expect post_id to be encoded properly expect(fetchMocker.mock.calls[0][0]).toBe("/blogposts/post%3Fid%20%3D%20%F0%9F%A5%B4"); }); + + it("multipart/form-data", async () => { + const client = createClient(); + mockFetchOnce({ status: 200, body: "{}" }); + const { data } = await client.put("/contact", { + headers: { + "Content-Type": "multipart/form-data", + }, + body: { + name: "John Doe", + email: "test@email.email", + subject: "Test Message", + message: "This is a test message", + }, + bodySerializer(body) { + const fd = new FormData(); + for (const [k, v] of Object.entries(body)) { + fd.append(k, v); + } + return fd; + }, + }); + + // expect post_id to be encoded properly + const req = fetchMocker.mock.calls[0][1]; + expect(req.body).toBeInstanceOf(FormData); + }); }); describe("responses", () => { @@ -294,17 +325,6 @@ describe("client", () => { expect(error.message).toBe("An unexpected error occurred"); }); - it("falls back to text() on invalid JSON", async () => { - const client = createClient(); - const bodyResponse = "My Post"; - mockFetchOnce({ status: 200, body: bodyResponse }); - const { data, error } = await client.get("/blogposts/{post_id}", { params: { path: { post_id: "my-post" } } }); - if (error) throw new Error("falls back to text(): error shouldn’t be present"); - - // assert `data` is a string - expect(data).toBe(bodyResponse); - }); - describe("parseAs", () => { it("text", async () => { const client = createClient(); @@ -498,7 +518,6 @@ describe("client", () => { const client = createClient(); mockFetchOnce({ status: 201, body: JSON.stringify(mockData) }); const { data, error, response } = await client.put("/blogposts", { - params: {}, body: { title: "New Post", body: "

Best post yet

", diff --git a/packages/openapi-fetch/src/index.ts b/packages/openapi-fetch/src/index.ts index 54fff0ee1..d718d9290 100644 --- a/packages/openapi-fetch/src/index.ts +++ b/packages/openapi-fetch/src/index.ts @@ -12,6 +12,8 @@ interface ClientOptions extends RequestInit { fetch?: typeof fetch; /** global querySerializer */ querySerializer?: QuerySerializer; + /** global bodySerializer */ + bodySerializer?: BodySerializer; } export interface BaseParams { params?: { query?: Record }; @@ -38,38 +40,48 @@ export type PathsWith, PathnameMeth }[keyof Paths]; /** Find first match of multiple keys */ export type FilterKeys = { [K in keyof Obj]: K extends Matchers ? Obj[K] : never }[keyof Obj]; -/** handle "application/json", "application/vnd.api+json", "appliacation/json;charset=utf-8" and more */ -export type JSONLike = `${string}json${string}`; +export type MediaType = `${string}/${string}`; // general purpose types -export type Params = O extends { parameters: any } ? { params: NonNullable } : BaseParams; -export type RequestBodyObj = O extends { requestBody?: any } ? O["requestBody"] : never; -export type RequestBodyContent = undefined extends RequestBodyObj ? FilterKeys>, "content"> | undefined : FilterKeys, "content">; -export type RequestBodyJSON = FilterKeys, JSONLike> extends never ? FilterKeys>, JSONLike> | undefined : FilterKeys, JSONLike>; -export type RequestBody = undefined extends RequestBodyJSON ? { body?: RequestBodyJSON } : { body: RequestBodyJSON }; -export type QuerySerializer = (query: O extends { parameters: any } ? NonNullable : Record) => string; -export type RequestOptions = Params & RequestBody & { querySerializer?: QuerySerializer; parseAs?: ParseAs }; -export type Success = FilterKeys, "content">; -export type Error = FilterKeys, "content">; +export type Params = T extends { parameters: any } ? { params: NonNullable } : BaseParams; +export type RequestBodyObj = T extends { requestBody?: any } ? T["requestBody"] : never; +export type RequestBodyContent = undefined extends RequestBodyObj ? FilterKeys>, "content"> | undefined : FilterKeys, "content">; +export type RequestBodyMedia = FilterKeys, MediaType> extends never ? FilterKeys>, MediaType> | undefined : FilterKeys, MediaType>; +export type RequestBody = undefined extends RequestBodyMedia ? { body?: RequestBodyMedia } : { body: RequestBodyMedia }; +export type QuerySerializer = (query: T extends { parameters: any } ? NonNullable : Record) => string; +export type BodySerializer = (body: RequestBodyMedia) => any; +export type RequestOptions = Params & + RequestBody & { + querySerializer?: QuerySerializer; + bodySerializer?: BodySerializer; + parseAs?: ParseAs; + }; +export type Success = FilterKeys, "content">; +export type Error = FilterKeys, "content">; // fetch types export type FetchOptions = RequestOptions & Omit; export type FetchResponse = - | { data: T extends { responses: any } ? NonNullable, JSONLike>> : unknown; error?: never; response: Response } - | { data?: never; error: T extends { responses: any } ? NonNullable, JSONLike>> : unknown; response: Response }; + | { data: T extends { responses: any } ? NonNullable, MediaType>> : unknown; error?: never; response: Response } + | { data?: never; error: T extends { responses: any } ? NonNullable, MediaType>> : unknown; response: Response }; -/** Call URLSearchParams() on the object, but remove `undefined` and `null` params */ -export function defaultSerializer(q: unknown): string { +/** serialize query params to string */ +export function defaultQuerySerializer(q: T): string { const search = new URLSearchParams(); if (q && typeof q === "object") { for (const [k, v] of Object.entries(q)) { if (v === undefined || v === null) continue; - search.set(k, String(v)); + search.set(k, v); } } return search.toString(); } +/** serialize body object to string */ +export function defaultBodySerializer(body: T): string { + return JSON.stringify(body); +} + /** Construct URL string from baseUrl and handle path and query params */ export function createFinalURL(url: string, options: { baseUrl?: string; params: { query?: Record; path?: Record }; querySerializer: QuerySerializer }): string { let finalURL = `${options.baseUrl ? options.baseUrl.replace(TRAILING_SLASH_RE, "") : ""}${url as string}`; @@ -84,7 +96,7 @@ export function createFinalURL(url: string, options: { baseUrl?: string; para } export default function createClient(clientOptions: ClientOptions = {}) { - const { fetch = globalThis.fetch, querySerializer: globalQuerySerializer, ...options } = clientOptions; + const { fetch = globalThis.fetch, querySerializer: globalQuerySerializer, bodySerializer: globalBodySerializer, ...options } = clientOptions; const defaultHeaders = new Headers({ ...DEFAULT_HEADERS, @@ -92,7 +104,7 @@ export default function createClient(clientOptions: ClientOpti }); async function coreFetch

(url: P, fetchOptions: FetchOptions): Promise> { - const { headers, body: requestBody, params = {}, parseAs = "json", querySerializer = globalQuerySerializer ?? defaultSerializer, ...init } = fetchOptions || {}; + const { headers, body: requestBody, params = {}, parseAs = "json", querySerializer = globalQuerySerializer ?? defaultQuerySerializer, bodySerializer = globalBodySerializer ?? defaultBodySerializer, ...init } = fetchOptions || {}; // URL const finalURL = createFinalURL(url as string, { baseUrl: options.baseUrl, params, querySerializer }); @@ -106,13 +118,14 @@ export default function createClient(clientOptions: ClientOpti } // fetch! - const response = await fetch(finalURL, { + const requestInit: RequestInit = { redirect: "follow", ...options, ...init, headers: baseHeaders, - body: typeof requestBody === "string" ? requestBody : JSON.stringify(requestBody), - }); + }; + if (requestBody) requestInit.body = bodySerializer(requestBody as any); + const response = await fetch(finalURL, requestInit); // handle empty content // note: we return `{}` because we want user truthy checks for `.data` or `.error` to succeed @@ -124,11 +137,8 @@ export default function createClient(clientOptions: ClientOpti if (response.ok) { let data: any = response.body; if (parseAs !== "stream") { - try { - data = await response.clone()[parseAs](); - } catch { - data = await response.clone().text(); - } + const cloned = response.clone(); + data = typeof cloned[parseAs] === "function" ? await cloned[parseAs]() : await cloned.text(); } return { data, response }; } diff --git a/packages/openapi-fetch/test/v1.d.ts b/packages/openapi-fetch/test/v1.d.ts index 29f165cf5..60ed4d2ba 100644 --- a/packages/openapi-fetch/test/v1.d.ts +++ b/packages/openapi-fetch/test/v1.d.ts @@ -221,6 +221,14 @@ export interface paths { }; }; }; + "/contact": { + put: { + requestBody: components["requestBodies"]["Contact"]; + responses: { + 200: components["responses"]["Contact"]; + }; + }; + }; } export type webhooks = Record; @@ -266,6 +274,11 @@ export interface components { }; }; }; + Contact: { + content: { + "text/html": string; + }; + }; Error: { content: { "application/json": { @@ -339,6 +352,16 @@ export interface components { }; }; }; + Contact: { + content: { + "multipart/form-data": { + name: string; + email: string; + subject: string; + message: string; + }; + }; + }; PatchPost: { content: { "application/json": { diff --git a/packages/openapi-fetch/test/v1.yaml b/packages/openapi-fetch/test/v1.yaml index 2ce08de63..c22075742 100644 --- a/packages/openapi-fetch/test/v1.yaml +++ b/packages/openapi-fetch/test/v1.yaml @@ -208,7 +208,13 @@ paths: $ref: '#/components/responses/Error' 500: $ref: '#/components/responses/Error' - + /contact: + put: + requestBody: + $ref: '#/components/requestBodies/Contact' + responses: + 200: + $ref: '#/components/responses/Contact' components: schemas: Post: @@ -295,6 +301,26 @@ components: required: - message - replied_at + Contact: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + name: + type: string + email: + type: string + subject: + type: string + message: + type: string + required: + - name + - email + - subject + - message PatchPost: required: true content: @@ -347,6 +373,11 @@ components: type: string required: - message + Contact: + content: + text/html: + schema: + type: string Error: content: application/json: From 457cd9563d21d4d36d8c0caa55b699684971eab1 Mon Sep 17 00:00:00 2001 From: Drew Powers Date: Tue, 27 Jun 2023 23:54:56 -0700 Subject: [PATCH 2/2] Drop Node 16 tests --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 837d04b96..a437e77ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [16.x, 18.x, 20.x] + node-version: [18.x, 20.x] steps: - uses: actions/checkout@v3 - uses: pnpm/action-setup@v2