fork of hey-api/openapi-ts because I need some additional things

docs: update client docs

Lubos ​ 39eee4a8 723f1820

+12093 -782
+5 -12
README.md
··· 120 120 ```sh 121 121 npx @hey-api/openapi-ts \ 122 122 -i https://get.heyapi.dev/hey-api/backend \ 123 - -o src/client \ 124 - -c @hey-api/client-fetch 123 + -o src/client 125 124 ``` 126 125 127 126 Congratulations on creating your first client! 🎉 You can learn more about the generated files on the [Output](https://heyapi.dev/openapi-ts/output) page. 128 - 129 - Before you can make API requests with the client you've just created, you need to install `@hey-api/client-fetch` and configure it. 130 127 131 128 ## Installation 132 129 133 130 #### npm 134 131 135 132 ```sh 136 - npm install @hey-api/client-fetch && npm install @hey-api/openapi-ts -D 133 + npm install @hey-api/openapi-ts -D 137 134 ``` 138 135 139 136 #### pnpm 140 137 141 138 ```sh 142 - pnpm add @hey-api/client-fetch && pnpm add @hey-api/openapi-ts -D 139 + pnpm add @hey-api/openapi-ts -D 143 140 ``` 144 141 145 142 #### yarn 146 143 147 144 ```sh 148 - yarn add @hey-api/client-fetch && yarn add @hey-api/openapi-ts -D 145 + yarn add @hey-api/openapi-ts -D 149 146 ``` 150 147 151 148 #### bun 152 149 153 150 ```sh 154 - bun add @hey-api/client-fetch && bun add @hey-api/openapi-ts -D 151 + bun add @hey-api/openapi-ts -D 155 152 ``` 156 153 157 154 We recommend pinning an exact version so you can safely upgrade when you're ready. This package is in [initial development](https://semver.org/spec/v0.1.0.html#spec-item-5) and its API might change before v1. ··· 178 175 createClient({ 179 176 input: 'https://get.heyapi.dev/hey-api/backend', 180 177 output: 'src/client', 181 - plugins: ['@hey-api/client-fetch'], 182 178 }); 183 179 ``` 184 180 ··· 194 190 export default defineConfig({ 195 191 input: 'https://get.heyapi.dev/hey-api/backend', 196 192 output: 'src/client', 197 - plugins: ['@hey-api/client-fetch'], 198 193 }); 199 194 ``` 200 195 ··· 205 200 module.exports = { 206 201 input: 'https://get.heyapi.dev/hey-api/backend', 207 202 output: 'src/client', 208 - plugins: ['@hey-api/client-fetch'], 209 203 }; 210 204 ``` 211 205 ··· 216 210 export default { 217 211 input: 'https://get.heyapi.dev/hey-api/backend', 218 212 output: 'src/client', 219 - plugins: ['@hey-api/client-fetch'], 220 213 }; 221 214 ``` 222 215
+4
docs/.vitepress/config/en.ts
··· 42 42 text: 'SDK', 43 43 }, 44 44 { 45 + link: '/openapi-ts/output/client', 46 + text: 'Client', 47 + }, 48 + { 45 49 link: '/openapi-ts/output/json-schema', 46 50 text: 'JSON Schema', 47 51 },
+1 -40
docs/openapi-ts/clients/axios.md
··· 35 35 36 36 ## Installation 37 37 38 - Start by adding `@hey-api/client-axios` to your dependencies. 39 - 40 - ::: code-group 41 - 42 - ```sh [npm] 43 - npm install @hey-api/client-axios 44 - ``` 45 - 46 - ```sh [pnpm] 47 - pnpm add @hey-api/client-axios 48 - ``` 49 - 50 - ```sh [yarn] 51 - yarn add @hey-api/client-axios 52 - ``` 53 - 54 - ```sh [bun] 55 - bun add @hey-api/client-axios 56 - ``` 57 - 58 - ::: 59 - 60 38 In your [configuration](/openapi-ts/get-started), add `@hey-api/client-axios` to your plugins and you'll be ready to generate client artifacts. :tada: 61 39 62 40 ::: code-group ··· 137 115 You can also create your own client instance. You can use it to manually send requests or point it to a different domain. 138 116 139 117 ```js 140 - import { createClient } from '@hey-api/client-axios'; 118 + import { createClient } from './client/client'; 141 119 142 120 const myClient = createClient({ 143 121 baseURL: 'https://example.com', ··· 226 204 url: '/foo/{fooId}', 227 205 }); 228 206 console.log(url); // prints '/foo/1?bar=baz' 229 - ``` 230 - 231 - ## Bundling 232 - 233 - Sometimes, you may not want to declare client packages as a dependency. This scenario is common if you're using Hey API to generate output that is repackaged and published for other consumers under your own brand. For such cases, our clients support bundling through the `client.bundle` configuration option. 234 - 235 - ```js 236 - export default { 237 - input: 'https://get.heyapi.dev/hey-api/backend', 238 - output: 'src/client', 239 - plugins: [ 240 - { 241 - bundle: true, // [!code ++] 242 - name: '@hey-api/client-axios', 243 - }, 244 - ], 245 - }; 246 207 ``` 247 208 248 209 <!--@include: ../../examples.md-->
+7 -40
docs/openapi-ts/clients/fetch.md
··· 35 35 36 36 ## Installation 37 37 38 - Start by adding `@hey-api/client-fetch` to your dependencies. 39 - 40 - ::: code-group 41 - 42 - ```sh [npm] 43 - npm install @hey-api/client-fetch 44 - ``` 45 - 46 - ```sh [pnpm] 47 - pnpm add @hey-api/client-fetch 48 - ``` 49 - 50 - ```sh [yarn] 51 - yarn add @hey-api/client-fetch 52 - ``` 53 - 54 - ```sh [bun] 55 - bun add @hey-api/client-fetch 56 - ``` 57 - 58 - ::: 59 - 60 38 In your [configuration](/openapi-ts/get-started), add `@hey-api/client-fetch` to your plugins and you'll be ready to generate client artifacts. :tada: 61 39 62 40 ::: code-group ··· 75 53 -o src/client \ 76 54 -c @hey-api/client-fetch # [!code ++] 77 55 ``` 56 + 57 + ::: 58 + 59 + ::: tip 60 + 61 + This step is optional because Fetch is the default client. 78 62 79 63 ::: 80 64 ··· 137 121 You can also create your own client instance. You can use it to manually send requests or point it to a different domain. 138 122 139 123 ```js 140 - import { createClient } from '@hey-api/client-fetch'; 124 + import { createClient } from './client/client'; 141 125 142 126 const myClient = createClient({ 143 127 baseUrl: 'https://example.com', ··· 297 281 url: '/foo/{fooId}', 298 282 }); 299 283 console.log(url); // prints '/foo/1?bar=baz' 300 - ``` 301 - 302 - ## Bundling 303 - 304 - Sometimes, you may not want to declare client packages as a dependency. This scenario is common if you're using Hey API to generate output that is repackaged and published for other consumers under your own brand. For such cases, our clients support bundling through the `client.bundle` configuration option. 305 - 306 - ```js 307 - export default { 308 - input: 'https://get.heyapi.dev/hey-api/backend', 309 - output: 'src/client', 310 - plugins: [ 311 - { 312 - bundle: true, // [!code ++] 313 - name: '@hey-api/client-fetch', 314 - }, 315 - ], 316 - }; 317 284 ``` 318 285 319 286 <!--@include: ../../examples.md-->
+1 -40
docs/openapi-ts/clients/next-js.md
··· 29 29 30 30 ## Installation 31 31 32 - Start by adding `@hey-api/client-next` to your dependencies. 33 - 34 - ::: code-group 35 - 36 - ```sh [npm] 37 - npm install @hey-api/client-next 38 - ``` 39 - 40 - ```sh [pnpm] 41 - pnpm add @hey-api/client-next 42 - ``` 43 - 44 - ```sh [yarn] 45 - yarn add @hey-api/client-next 46 - ``` 47 - 48 - ```sh [bun] 49 - bun add @hey-api/client-next 50 - ``` 51 - 52 - ::: 53 - 54 32 In your [configuration](/openapi-ts/get-started), add `@hey-api/client-next` to your plugins and you'll be ready to generate client artifacts. :tada: 55 33 56 34 ::: code-group ··· 131 109 You can also create your own client instance. You can use it to manually send requests or point it to a different domain. 132 110 133 111 ```js 134 - import { createClient } from '@hey-api/client-next'; 112 + import { createClient } from './client/client'; 135 113 136 114 const myClient = createClient({ 137 115 baseUrl: 'https://example.com', ··· 290 268 url: '/foo/{fooId}', 291 269 }); 292 270 console.log(url); // prints '/foo/1?bar=baz' 293 - ``` 294 - 295 - ## Bundling 296 - 297 - Sometimes, you may not want to declare client packages as a dependency. This scenario is common if you're using Hey API to generate output that is repackaged and published for other consumers under your own brand. For such cases, our clients support bundling through the `client.bundle` configuration option. 298 - 299 - ```js 300 - export default { 301 - input: 'https://get.heyapi.dev/hey-api/backend', 302 - output: 'src/client', 303 - plugins: [ 304 - { 305 - bundle: true, // [!code ++] 306 - name: '@hey-api/client-next', 307 - }, 308 - ], 309 - }; 310 271 ``` 311 272 312 273 <!--@include: ../../examples.md-->
-31
docs/openapi-ts/configuration.md
··· 15 15 export default defineConfig({ 16 16 input: 'https://get.heyapi.dev/hey-api/backend', 17 17 output: 'src/client', 18 - plugins: ['@hey-api/client-fetch'], 19 18 }); 20 19 ``` 21 20 ··· 24 23 module.exports = { 25 24 input: 'https://get.heyapi.dev/hey-api/backend', 26 25 output: 'src/client', 27 - plugins: ['@hey-api/client-fetch'], 28 26 }; 29 27 ``` 30 28 ··· 33 31 export default { 34 32 input: 'https://get.heyapi.dev/hey-api/backend', 35 33 output: 'src/client', 36 - plugins: ['@hey-api/client-fetch'], 37 34 }; 38 35 ``` 39 36 ··· 109 106 validate_EXPERIMENTAL: true, // [!code ++] 110 107 }, 111 108 output: 'src/client', 112 - plugins: ['@hey-api/client-fetch'], 113 109 }; 114 110 ``` 115 111 ··· 126 122 format: false, // [!code ++] 127 123 path: 'src/client', 128 124 }, 129 - plugins: ['@hey-api/client-fetch'], 130 125 }; 131 126 ``` 132 127 ··· 137 132 format: 'prettier', // [!code ++] 138 133 path: 'src/client', 139 134 }, 140 - plugins: ['@hey-api/client-fetch'], 141 135 }; 142 136 ``` 143 137 ··· 148 142 format: 'biome', // [!code ++] 149 143 path: 'src/client', 150 144 }, 151 - plugins: ['@hey-api/client-fetch'], 152 145 }; 153 146 ``` 154 147 ··· 169 162 lint: false, // [!code ++] 170 163 path: 'src/client', 171 164 }, 172 - plugins: ['@hey-api/client-fetch'], 173 165 }; 174 166 ``` 175 167 ··· 180 172 lint: 'eslint', // [!code ++] 181 173 path: 'src/client', 182 174 }, 183 - plugins: ['@hey-api/client-fetch'], 184 175 }; 185 176 ``` 186 177 ··· 191 182 lint: 'biome', // [!code ++] 192 183 path: 'src/client', 193 184 }, 194 - plugins: ['@hey-api/client-fetch'], 195 185 }; 196 186 ``` 197 187 ··· 202 192 lint: 'oxlint', // [!code ++] 203 193 path: 'src/client', 204 194 }, 205 - plugins: ['@hey-api/client-fetch'], 206 195 }; 207 196 ``` 208 197 ··· 231 220 path: 'https://get.heyapi.dev/hey-api/backend', 232 221 }, 233 222 output: 'src/client', 234 - plugins: ['@hey-api/client-fetch'], 235 223 }; 236 224 ``` 237 225 ··· 246 234 path: 'https://get.heyapi.dev/hey-api/backend', 247 235 }, 248 236 output: 'src/client', 249 - plugins: ['@hey-api/client-fetch'], 250 237 }; 251 238 ``` 252 239 ··· 269 256 path: 'https://get.heyapi.dev/hey-api/backend', 270 257 }, 271 258 output: 'src/client', 272 - plugins: ['@hey-api/client-fetch'], 273 259 }; 274 260 ``` 275 261 ··· 284 270 path: 'https://get.heyapi.dev/hey-api/backend', 285 271 }, 286 272 output: 'src/client', 287 - plugins: ['@hey-api/client-fetch'], 288 273 }; 289 274 ``` 290 275 ··· 303 288 path: 'https://get.heyapi.dev/hey-api/backend', 304 289 }, 305 290 output: 'src/client', 306 - plugins: ['@hey-api/client-fetch'], 307 291 }; 308 292 ``` 309 293 ··· 324 308 path: 'https://get.heyapi.dev/hey-api/backend', 325 309 }, 326 310 output: 'src/client', 327 - plugins: ['@hey-api/client-fetch'], 328 311 }; 329 312 ``` 330 313 ··· 339 322 path: 'https://get.heyapi.dev/hey-api/backend', 340 323 }, 341 324 output: 'src/client', 342 - plugins: ['@hey-api/client-fetch'], 343 325 }; 344 326 ``` 345 327 ··· 362 344 path: 'https://get.heyapi.dev/hey-api/backend', 363 345 }, 364 346 output: 'src/client', 365 - plugins: ['@hey-api/client-fetch'], 366 347 }; 367 348 ``` 368 349 ··· 377 358 path: 'https://get.heyapi.dev/hey-api/backend', 378 359 }, 379 360 output: 'src/client', 380 - plugins: ['@hey-api/client-fetch'], 381 361 }; 382 362 ``` 383 363 ··· 400 380 path: 'https://get.heyapi.dev/hey-api/backend', 401 381 }, 402 382 output: 'src/client', 403 - plugins: ['@hey-api/client-fetch'], 404 383 }; 405 384 ``` 406 385 ··· 415 394 path: 'https://get.heyapi.dev/hey-api/backend', 416 395 }, 417 396 output: 'src/client', 418 - plugins: ['@hey-api/client-fetch'], 419 397 }; 420 398 ``` 421 399 ··· 438 416 path: 'https://get.heyapi.dev/hey-api/backend', 439 417 }, 440 418 output: 'src/client', 441 - plugins: ['@hey-api/client-fetch'], 442 419 }; 443 420 ``` 444 421 ··· 453 430 path: 'https://get.heyapi.dev/hey-api/backend', 454 431 }, 455 432 output: 'src/client', 456 - plugins: ['@hey-api/client-fetch'], 457 433 }; 458 434 ``` 459 435 ··· 472 448 path: 'https://get.heyapi.dev/hey-api/backend', 473 449 }, 474 450 output: 'src/client', 475 - plugins: ['@hey-api/client-fetch'], 476 451 }; 477 452 ``` 478 453 ··· 489 464 path: 'https://get.heyapi.dev/hey-api/backend', 490 465 }, 491 466 output: 'src/client', 492 - plugins: ['@hey-api/client-fetch'], 493 467 }; 494 468 ``` 495 469 ··· 506 480 path: 'https://get.heyapi.dev/hey-api/backend', 507 481 }, 508 482 output: 'src/client', 509 - plugins: ['@hey-api/client-fetch'], 510 483 }; 511 484 ``` 512 485 ··· 543 516 path: 'https://get.heyapi.dev/hey-api/backend', 544 517 }, 545 518 output: 'src/client', 546 - plugins: ['@hey-api/client-fetch'], 547 519 }; 548 520 ``` 549 521 ··· 564 536 watch: true, // [!code ++] 565 537 }, 566 538 output: 'src/client', 567 - plugins: ['@hey-api/client-fetch'], 568 539 }; 569 540 ``` 570 541 ··· 572 543 npx @hey-api/openapi-ts \ 573 544 -i https://get.heyapi.dev/hey-api/backend \ 574 545 -o src/client \ 575 - -c @hey-api/client-fetch \ 576 546 -w # [!code ++] 577 547 ``` 578 548 ··· 589 559 clean: false, // [!code ++] 590 560 path: 'src/client', 591 561 }, 592 - plugins: ['@hey-api/client-fetch'], 593 562 }; 594 563 ``` 595 564
+5 -9
docs/openapi-ts/get-started.md
··· 38 38 ```sh 39 39 npx @hey-api/openapi-ts \ 40 40 -i https://get.heyapi.dev/hey-api/backend \ 41 - -o src/client \ 42 - -c @hey-api/client-fetch 41 + -o src/client 43 42 ``` 44 43 45 44 Congratulations on creating your first client! 🎉 You can learn more about the generated files on the [Output](/openapi-ts/output) page. 46 45 47 - Before you can make API requests with the client you've just created, you need to install `@hey-api/client-fetch` and configure it. Let's start by declaring your dependencies. 48 - 49 46 ## Installation 50 47 51 48 ::: code-group 52 49 53 50 ```sh [npm] 54 - npm install @hey-api/client-fetch && npm install @hey-api/openapi-ts -D 51 + npm install @hey-api/openapi-ts -D 55 52 ``` 56 53 57 54 ```sh [pnpm] 58 - pnpm add @hey-api/client-fetch && pnpm add @hey-api/openapi-ts -D 55 + pnpm add @hey-api/openapi-ts -D 59 56 ``` 60 57 61 58 ```sh [yarn] 62 - yarn add @hey-api/client-fetch && yarn add @hey-api/openapi-ts -D 59 + yarn add @hey-api/openapi-ts -D 63 60 ``` 64 61 65 62 ```sh [bun] 66 - bun add @hey-api/client-fetch && bun add @hey-api/openapi-ts -D 63 + bun add @hey-api/openapi-ts -D 67 64 ``` 68 65 69 66 ::: ··· 94 91 createClient({ 95 92 input: 'https://get.heyapi.dev/hey-api/backend', 96 93 output: 'src/client', 97 - plugins: ['@hey-api/client-fetch'], 98 94 }); 99 95 ``` 100 96
+1 -2
docs/openapi-ts/integrations.md
··· 98 98 ```sh [Hey API] 99 99 npx @hey-api/openapi-ts \ 100 100 -i https://get.heyapi.dev/hey-api/backend \ 101 - -o src/client \ 102 - -c @hey-api/client-fetch 101 + -o src/client 103 102 ``` 104 103 105 104 ```sh [OpenAPI TypeScript]
+2 -18
docs/openapi-ts/output.md
··· 20 20 ├── node_modules/ 21 21 ├── src/ 22 22 │ ├── client/ 23 + │ │ ├── client/ 24 + │ │ ├── core/ 23 25 │ │ ├── client.gen.ts 24 26 │ │ ├── index.ts 25 27 │ │ ├── sdk.gen.ts ··· 66 68 indexFile: false, // [!code ++] 67 69 path: 'src/client', 68 70 }, 69 - plugins: ['@hey-api/client-fetch'], 70 71 }; 71 72 ``` 72 73 ··· 82 83 output: 'src/client', 83 84 plugins: [ 84 85 ...defaultPlugins, 85 - '@hey-api/client-fetch', 86 86 { 87 87 exportFromIndex: true, // [!code ++] 88 88 name: 'zod', ··· 94 94 ::: warning 95 95 Re-exporting additional files from index file may result in broken output due to naming conflicts. 96 96 ::: 97 - 98 - ## Client 99 - 100 - `client.gen.ts` is generated by [client plugins](/openapi-ts/clients). If you choose to generate SDKs (enabled by default), you must specify a client which will create this file. 101 - 102 - ::: code-group 103 - 104 - ```ts [client.gen.ts] 105 - import { createClient, createConfig } from '@hey-api/client-fetch'; 106 - 107 - export const client = createClient(createConfig()); 108 - ``` 109 - 110 - ::: 111 - 112 - The contents of this file are consumed by SDKs, but you can also import `client` in your application to perform additional configuration or send manual requests. 113 97 114 98 <!--@include: ../examples.md--> 115 99 <!--@include: ../sponsors.md-->
+27
docs/openapi-ts/output/client.md
··· 1 + --- 2 + title: Client 3 + description: Learn about files generated with @hey-api/openapi-ts. 4 + --- 5 + 6 + # Client 7 + 8 + `client.gen.ts` is generated by [client plugins](/openapi-ts/clients). If you choose to generate SDKs (enabled by default), we use the Fetch client unless specified otherwise. 9 + 10 + ::: code-group 11 + 12 + ```ts [client.gen.ts] 13 + import { createClient, createConfig } from './client'; 14 + 15 + export const client = createClient(createConfig()); 16 + ``` 17 + 18 + ::: 19 + 20 + The contents of this file are consumed by SDKs, but you can also import `client` in your application to perform additional configuration or send manual requests. 21 + 22 + ## Bundle 23 + 24 + Client plugins provide their bundles inside `client` and `core` folders. The contents of these folders don't depend on the provided input. Everything inside these folders serves as a scaffolding so the generated code can make HTTP requests. 25 + 26 + <!--@include: ../../examples.md--> 27 + <!--@include: ../../sponsors.md-->
-3
docs/openapi-ts/output/json-schema.md
··· 21 21 output: 'src/client', 22 22 plugins: [ 23 23 ...defaultPlugins, 24 - '@hey-api/client-fetch', 25 24 { 26 25 name: '@hey-api/schemas', 27 26 type: 'json', // [!code ++] ··· 38 37 output: 'src/client', 39 38 plugins: [ 40 39 ...defaultPlugins, 41 - '@hey-api/client-fetch', 42 40 { 43 41 name: '@hey-api/schemas', 44 42 type: 'form', // [!code ++] ··· 55 53 output: 'src/client', 56 54 plugins: [ 57 55 ...defaultPlugins, 58 - '@hey-api/client-fetch', 59 56 '@hey-api/schemas', // [!code --] 60 57 ], 61 58 };
+3 -8
docs/openapi-ts/output/sdk.md
··· 33 33 output: 'src/client', 34 34 plugins: [ 35 35 ...defaultPlugins, 36 - '@hey-api/client-fetch', 37 36 { 38 37 asClass: false, // default // [!code ++] 39 38 name: '@hey-api/sdk', ··· 50 49 output: 'src/client', 51 50 plugins: [ 52 51 ...defaultPlugins, 53 - '@hey-api/client-fetch', 54 52 { 55 53 asClass: true, // [!code ++] 56 54 name: '@hey-api/sdk', ··· 64 62 input: 'https://get.heyapi.dev/hey-api/backend', 65 63 output: 'src/client', 66 64 plugins: [ 67 - '@hey-api/client-fetch', 68 65 '@hey-api/typescript', 69 66 '@hey-api/sdk', // [!code --] 70 67 ], ··· 80 77 ::: code-group 81 78 82 79 ```ts [flat] 83 - import type { Options } from '@hey-api/client-fetch'; 84 - 80 + import type { Options } from './client'; 85 81 import { client as _heyApiClient } from './client.gen'; 86 82 import type { AddPetData, AddPetError, AddPetResponse } from './types.gen'; 87 83 ··· 93 89 ``` 94 90 95 91 ```ts [class] 96 - import type { Options } from '@hey-api/client-fetch'; 97 - 92 + import type { Options } from './client'; 98 93 import { client as _heyApiClient } from './client.gen'; 99 94 import type { AddPetData, AddPetError, AddPetResponse } from './types.gen'; 100 95 ··· 139 134 ``` 140 135 141 136 ```ts [none] 142 - import { client } from '@hey-api/client-fetch'; 137 + import { client } from './client/client'; 143 138 144 139 client.post({ 145 140 body: {
+1 -5
docs/openapi-ts/output/typescript.md
··· 32 32 33 33 ::: 34 34 35 - As you can see, everything is exported from `types.gen.ts`. You can import individual exports in your application and use them as necessary. 35 + Every generated interface inside `types.gen.ts` is exported. You can import individual exports in your application and use them as necessary. 36 36 37 37 ## Configuration 38 38 ··· 46 46 output: 'src/client', 47 47 plugins: [ 48 48 ...defaultPlugins, 49 - '@hey-api/client-fetch', 50 49 { 51 50 name: '@hey-api/typescript', 52 51 // ...custom options // [!code ++] ··· 69 68 output: 'src/client', 70 69 plugins: [ 71 70 ...defaultPlugins, 72 - '@hey-api/client-fetch', 73 71 { 74 72 enums: false, // default // [!code ++] 75 73 name: '@hey-api/typescript', ··· 86 84 output: 'src/client', 87 85 plugins: [ 88 86 ...defaultPlugins, 89 - '@hey-api/client-fetch', 90 87 { 91 88 enums: 'javascript', // [!code ++] 92 89 name: '@hey-api/typescript', ··· 103 100 output: 'src/client', 104 101 plugins: [ 105 102 ...defaultPlugins, 106 - '@hey-api/client-fetch', 107 103 { 108 104 enums: 'typescript', // [!code ++] 109 105 name: '@hey-api/typescript',
-1
docs/openapi-ts/plugins/custom.md
··· 170 170 input: 'https://get.heyapi.dev/hey-api/backend', 171 171 output: 'src/client', 172 172 plugins: [ 173 - '@hey-api/client-fetch', 174 173 defineConfig({ 175 174 myOption: true, 176 175 }),
-1
docs/openapi-ts/plugins/fastify.md
··· 43 43 output: 'src/client', 44 44 plugins: [ 45 45 ...defaultPlugins, 46 - '@hey-api/client-fetch', 47 46 'fastify', // [!code ++] 48 47 ], 49 48 };
-5
docs/openapi-ts/plugins/tanstack-query.md
··· 44 44 output: 'src/client', 45 45 plugins: [ 46 46 ...defaultPlugins, 47 - '@hey-api/client-fetch', 48 47 '@tanstack/react-query', // [!code ++] 49 48 ], 50 49 }; ··· 58 57 output: 'src/client', 59 58 plugins: [ 60 59 ...defaultPlugins, 61 - '@hey-api/client-fetch', 62 60 '@tanstack/vue-query', // [!code ++] 63 61 ], 64 62 }; ··· 72 70 output: 'src/client', 73 71 plugins: [ 74 72 ...defaultPlugins, 75 - '@hey-api/client-fetch', 76 73 '@tanstack/angular-query-experimental', // [!code ++] 77 74 ], 78 75 }; ··· 86 83 output: 'src/client', 87 84 plugins: [ 88 85 ...defaultPlugins, 89 - '@hey-api/client-fetch', 90 86 '@tanstack/svelte-query', // [!code ++] 91 87 ], 92 88 }; ··· 100 96 output: 'src/client', 101 97 plugins: [ 102 98 ...defaultPlugins, 103 - '@hey-api/client-fetch', 104 99 '@tanstack/solid-query', // [!code ++] 105 100 ], 106 101 };
-2
docs/openapi-ts/plugins/valibot.md
··· 40 40 output: 'src/client', 41 41 plugins: [ 42 42 ...defaultPlugins, 43 - '@hey-api/client-fetch', 44 43 'valibot', // [!code ++] 45 44 ], 46 45 }; ··· 58 57 output: 'src/client', 59 58 plugins: [ 60 59 ...defaultPlugins, 61 - '@hey-api/client-fetch', 62 60 'valibot', 63 61 { 64 62 name: '@hey-api/sdk', // [!code ++]
-2
docs/openapi-ts/plugins/zod.md
··· 38 38 output: 'src/client', 39 39 plugins: [ 40 40 // ...other plugins 41 - '@hey-api/client-fetch', 42 41 'zod', // [!code ++] 43 42 ], 44 43 }; ··· 54 53 output: 'src/client', 55 54 plugins: [ 56 55 // ...other plugins 57 - '@hey-api/client-fetch', 58 56 'zod', 59 57 { 60 58 name: '@hey-api/sdk', // [!code ++]
-3
docs/openapi-ts/transformers.md
··· 39 39 output: 'src/client', 40 40 plugins: [ 41 41 ...defaultPlugins, 42 - '@hey-api/client-fetch', 43 42 '@hey-api/transformers', // [!code ++] 44 43 ], 45 44 }; ··· 57 56 output: 'src/client', 58 57 plugins: [ 59 58 ...defaultPlugins, 60 - '@hey-api/client-fetch', 61 59 '@hey-api/transformers', 62 60 { 63 61 name: '@hey-api/sdk', // [!code ++] ··· 79 77 output: 'src/client', 80 78 plugins: [ 81 79 ...defaultPlugins, 82 - '@hey-api/client-fetch', 83 80 { 84 81 dates: true, // [!code ++] 85 82 name: '@hey-api/transformers',
-2
docs/openapi-ts/validators.md
··· 34 34 input: 'https://get.heyapi.dev/hey-api/backend', 35 35 output: 'src/client', 36 36 plugins: [ 37 - '@hey-api/client-fetch', 38 37 { 39 38 name: '@hey-api/sdk', 40 39 validator: 'zod', // [!code ++] ··· 48 47 input: 'https://get.heyapi.dev/hey-api/backend', 49 48 output: 'src/client', 50 49 plugins: [ 51 - '@hey-api/client-fetch', 52 50 { 53 51 name: '@hey-api/sdk', 54 52 validator: true, // [!code ++]
-1
examples/openapi-ts-axios/package.json
··· 12 12 "typecheck": "tsc --noEmit" 13 13 }, 14 14 "dependencies": { 15 - "@hey-api/client-axios": "workspace:*", 16 15 "@radix-ui/react-form": "0.1.1", 17 16 "@radix-ui/react-icons": "1.3.2", 18 17 "@radix-ui/themes": "3.1.6",
+1 -1
examples/openapi-ts-axios/src/App.tsx
··· 1 1 import './App.css'; 2 2 3 - import { createClient } from '@hey-api/client-axios'; 4 3 import * as Form from '@radix-ui/react-form'; 5 4 import { DownloadIcon, PlusIcon, ReloadIcon } from '@radix-ui/react-icons'; 6 5 import { ··· 17 16 } from '@radix-ui/themes'; 18 17 import { useState } from 'react'; 19 18 19 + import { createClient } from './client/client'; 20 20 import { PetSchema } from './client/schemas.gen'; 21 21 import { addPet, getPetById, updatePet } from './client/sdk.gen'; 22 22 import type { Pet } from './client/types.gen';
+1 -2
examples/openapi-ts-axios/src/client/client.gen.ts
··· 5 5 type Config, 6 6 createClient, 7 7 createConfig, 8 - } from '@hey-api/client-axios'; 9 - 8 + } from './client'; 10 9 import type { ClientOptions } from './types.gen'; 11 10 12 11 /**
+111
examples/openapi-ts-axios/src/client/client/client.ts
··· 1 + import type { AxiosError, RawAxiosRequestHeaders } from 'axios'; 2 + import axios from 'axios'; 3 + 4 + import type { Client, Config } from './types'; 5 + import { 6 + buildUrl, 7 + createConfig, 8 + mergeConfigs, 9 + mergeHeaders, 10 + setAuthParams, 11 + } from './utils'; 12 + 13 + export const createClient = (config: Config = {}): Client => { 14 + let _config = mergeConfigs(createConfig(), config); 15 + 16 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 17 + const { auth, ...configWithoutAuth } = _config; 18 + const instance = axios.create(configWithoutAuth); 19 + 20 + const getConfig = (): Config => ({ ..._config }); 21 + 22 + const setConfig = (config: Config): Config => { 23 + _config = mergeConfigs(_config, config); 24 + instance.defaults = { 25 + ...instance.defaults, 26 + ..._config, 27 + // @ts-expect-error 28 + headers: mergeHeaders(instance.defaults.headers, _config.headers), 29 + }; 30 + return getConfig(); 31 + }; 32 + 33 + // @ts-expect-error 34 + const request: Client['request'] = async (options) => { 35 + const opts = { 36 + ..._config, 37 + ...options, 38 + axios: options.axios ?? _config.axios ?? instance, 39 + headers: mergeHeaders(_config.headers, options.headers), 40 + }; 41 + 42 + if (opts.security) { 43 + await setAuthParams({ 44 + ...opts, 45 + security: opts.security, 46 + }); 47 + } 48 + 49 + if (opts.body && opts.bodySerializer) { 50 + opts.body = opts.bodySerializer(opts.body); 51 + } 52 + 53 + const url = buildUrl(opts); 54 + 55 + try { 56 + // assign Axios here for consistency with fetch 57 + const _axios = opts.axios!; 58 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 59 + const { auth, ...optsWithoutAuth } = opts; 60 + const response = await _axios({ 61 + ...optsWithoutAuth, 62 + baseURL: opts.baseURL as string, 63 + data: opts.body, 64 + headers: opts.headers as RawAxiosRequestHeaders, 65 + // let `paramsSerializer()` handle query params if it exists 66 + params: opts.paramsSerializer ? opts.query : undefined, 67 + url, 68 + }); 69 + 70 + let { data } = response; 71 + 72 + if (opts.responseType === 'json') { 73 + if (opts.responseValidator) { 74 + await opts.responseValidator(data); 75 + } 76 + 77 + if (opts.responseTransformer) { 78 + data = await opts.responseTransformer(data); 79 + } 80 + } 81 + 82 + return { 83 + ...response, 84 + data: data ?? {}, 85 + }; 86 + } catch (error) { 87 + const e = error as AxiosError; 88 + if (opts.throwOnError) { 89 + throw e; 90 + } 91 + // @ts-expect-error 92 + e.error = e.response?.data ?? {}; 93 + return e; 94 + } 95 + }; 96 + 97 + return { 98 + buildUrl, 99 + delete: (options) => request({ ...options, method: 'DELETE' }), 100 + get: (options) => request({ ...options, method: 'GET' }), 101 + getConfig, 102 + head: (options) => request({ ...options, method: 'HEAD' }), 103 + instance, 104 + options: (options) => request({ ...options, method: 'OPTIONS' }), 105 + patch: (options) => request({ ...options, method: 'PATCH' }), 106 + post: (options) => request({ ...options, method: 'POST' }), 107 + put: (options) => request({ ...options, method: 'PUT' }), 108 + request, 109 + setConfig, 110 + } as Client; 111 + };
+21
examples/openapi-ts-axios/src/client/client/index.ts
··· 1 + export type { Auth } from '../core/auth'; 2 + export type { QuerySerializerOptions } from '../core/bodySerializer'; 3 + export { 4 + formDataBodySerializer, 5 + jsonBodySerializer, 6 + urlSearchParamsBodySerializer, 7 + } from '../core/bodySerializer'; 8 + export { buildClientParams } from '../core/params'; 9 + export { createClient } from './client'; 10 + export type { 11 + Client, 12 + ClientOptions, 13 + Config, 14 + CreateClientConfig, 15 + Options, 16 + OptionsLegacyParser, 17 + RequestOptions, 18 + RequestResult, 19 + TDataShape, 20 + } from './types'; 21 + export { createConfig } from './utils';
+175
examples/openapi-ts-axios/src/client/client/types.ts
··· 1 + import type { 2 + AxiosError, 3 + AxiosInstance, 4 + AxiosResponse, 5 + AxiosStatic, 6 + CreateAxiosDefaults, 7 + } from 'axios'; 8 + 9 + import type { Auth } from '../core/auth'; 10 + import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; 11 + 12 + export interface Config<T extends ClientOptions = ClientOptions> 13 + extends Omit<CreateAxiosDefaults, 'auth' | 'baseURL' | 'headers' | 'method'>, 14 + CoreConfig { 15 + /** 16 + * Axios implementation. You can use this option to provide a custom 17 + * Axios instance. 18 + * 19 + * @default axios 20 + */ 21 + axios?: AxiosStatic; 22 + /** 23 + * Base URL for all requests made by this client. 24 + */ 25 + baseURL?: T['baseURL']; 26 + /** 27 + * An object containing any HTTP headers that you want to pre-populate your 28 + * `Headers` object with. 29 + * 30 + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} 31 + */ 32 + headers?: 33 + | CreateAxiosDefaults['headers'] 34 + | Record< 35 + string, 36 + | string 37 + | number 38 + | boolean 39 + | (string | number | boolean)[] 40 + | null 41 + | undefined 42 + | unknown 43 + >; 44 + /** 45 + * Throw an error instead of returning it in the response? 46 + * 47 + * @default false 48 + */ 49 + throwOnError?: T['throwOnError']; 50 + } 51 + 52 + export interface RequestOptions< 53 + ThrowOnError extends boolean = boolean, 54 + Url extends string = string, 55 + > extends Config<{ 56 + throwOnError: ThrowOnError; 57 + }> { 58 + /** 59 + * Any body that you want to add to your request. 60 + * 61 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} 62 + */ 63 + body?: unknown; 64 + path?: Record<string, unknown>; 65 + query?: Record<string, unknown>; 66 + /** 67 + * Security mechanism(s) to use for the request. 68 + */ 69 + security?: ReadonlyArray<Auth>; 70 + url: Url; 71 + } 72 + 73 + export type RequestResult< 74 + TData = unknown, 75 + TError = unknown, 76 + ThrowOnError extends boolean = boolean, 77 + > = ThrowOnError extends true 78 + ? Promise< 79 + AxiosResponse< 80 + TData extends Record<string, unknown> ? TData[keyof TData] : TData 81 + > 82 + > 83 + : Promise< 84 + | (AxiosResponse< 85 + TData extends Record<string, unknown> ? TData[keyof TData] : TData 86 + > & { error: undefined }) 87 + | (AxiosError< 88 + TError extends Record<string, unknown> ? TError[keyof TError] : TError 89 + > & { 90 + data: undefined; 91 + error: TError extends Record<string, unknown> 92 + ? TError[keyof TError] 93 + : TError; 94 + }) 95 + >; 96 + 97 + export interface ClientOptions { 98 + baseURL?: string; 99 + throwOnError?: boolean; 100 + } 101 + 102 + type MethodFn = < 103 + TData = unknown, 104 + TError = unknown, 105 + ThrowOnError extends boolean = false, 106 + >( 107 + options: Omit<RequestOptions<ThrowOnError>, 'method'>, 108 + ) => RequestResult<TData, TError, ThrowOnError>; 109 + 110 + type RequestFn = < 111 + TData = unknown, 112 + TError = unknown, 113 + ThrowOnError extends boolean = false, 114 + >( 115 + options: Omit<RequestOptions<ThrowOnError>, 'method'> & 116 + Pick<Required<RequestOptions<ThrowOnError>>, 'method'>, 117 + ) => RequestResult<TData, TError, ThrowOnError>; 118 + 119 + type BuildUrlFn = < 120 + TData extends { 121 + body?: unknown; 122 + path?: Record<string, unknown>; 123 + query?: Record<string, unknown>; 124 + url: string; 125 + }, 126 + >( 127 + options: Pick<TData, 'url'> & Omit<Options<TData>, 'axios'>, 128 + ) => string; 129 + 130 + export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & { 131 + instance: AxiosInstance; 132 + }; 133 + 134 + /** 135 + * The `createClientConfig()` function will be called on client initialization 136 + * and the returned object will become the client's initial configuration. 137 + * 138 + * You may want to initialize your client this way instead of calling 139 + * `setConfig()`. This is useful for example if you're using Next.js 140 + * to ensure your client always has the correct values. 141 + */ 142 + export type CreateClientConfig<T extends ClientOptions = ClientOptions> = ( 143 + override?: Config<ClientOptions & T>, 144 + ) => Config<Required<ClientOptions> & T>; 145 + 146 + export interface TDataShape { 147 + body?: unknown; 148 + headers?: unknown; 149 + path?: unknown; 150 + query?: unknown; 151 + url: string; 152 + } 153 + 154 + type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>; 155 + 156 + export type Options< 157 + TData extends TDataShape = TDataShape, 158 + ThrowOnError extends boolean = boolean, 159 + > = OmitKeys<RequestOptions<ThrowOnError>, 'body' | 'path' | 'query' | 'url'> & 160 + Omit<TData, 'url'>; 161 + 162 + export type OptionsLegacyParser< 163 + TData = unknown, 164 + ThrowOnError extends boolean = boolean, 165 + > = TData extends { body?: any } 166 + ? TData extends { headers?: any } 167 + ? OmitKeys<RequestOptions<ThrowOnError>, 'body' | 'headers' | 'url'> & TData 168 + : OmitKeys<RequestOptions<ThrowOnError>, 'body' | 'url'> & 169 + TData & 170 + Pick<RequestOptions<ThrowOnError>, 'headers'> 171 + : TData extends { headers?: any } 172 + ? OmitKeys<RequestOptions<ThrowOnError>, 'headers' | 'url'> & 173 + TData & 174 + Pick<RequestOptions<ThrowOnError>, 'body'> 175 + : OmitKeys<RequestOptions<ThrowOnError>, 'url'> & TData;
+286
examples/openapi-ts-axios/src/client/client/utils.ts
··· 1 + import { getAuthToken } from '../core/auth'; 2 + import type { 3 + QuerySerializer, 4 + QuerySerializerOptions, 5 + } from '../core/bodySerializer'; 6 + import type { ArraySeparatorStyle } from '../core/pathSerializer'; 7 + import { 8 + serializeArrayParam, 9 + serializeObjectParam, 10 + serializePrimitiveParam, 11 + } from '../core/pathSerializer'; 12 + import type { Client, ClientOptions, Config, RequestOptions } from './types'; 13 + 14 + interface PathSerializer { 15 + path: Record<string, unknown>; 16 + url: string; 17 + } 18 + 19 + const PATH_PARAM_RE = /\{[^{}]+\}/g; 20 + 21 + const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { 22 + let url = _url; 23 + const matches = _url.match(PATH_PARAM_RE); 24 + if (matches) { 25 + for (const match of matches) { 26 + let explode = false; 27 + let name = match.substring(1, match.length - 1); 28 + let style: ArraySeparatorStyle = 'simple'; 29 + 30 + if (name.endsWith('*')) { 31 + explode = true; 32 + name = name.substring(0, name.length - 1); 33 + } 34 + 35 + if (name.startsWith('.')) { 36 + name = name.substring(1); 37 + style = 'label'; 38 + } else if (name.startsWith(';')) { 39 + name = name.substring(1); 40 + style = 'matrix'; 41 + } 42 + 43 + const value = path[name]; 44 + 45 + if (value === undefined || value === null) { 46 + continue; 47 + } 48 + 49 + if (Array.isArray(value)) { 50 + url = url.replace( 51 + match, 52 + serializeArrayParam({ explode, name, style, value }), 53 + ); 54 + continue; 55 + } 56 + 57 + if (typeof value === 'object') { 58 + url = url.replace( 59 + match, 60 + serializeObjectParam({ 61 + explode, 62 + name, 63 + style, 64 + value: value as Record<string, unknown>, 65 + valueOnly: true, 66 + }), 67 + ); 68 + continue; 69 + } 70 + 71 + if (style === 'matrix') { 72 + url = url.replace( 73 + match, 74 + `;${serializePrimitiveParam({ 75 + name, 76 + value: value as string, 77 + })}`, 78 + ); 79 + continue; 80 + } 81 + 82 + const replaceValue = encodeURIComponent( 83 + style === 'label' ? `.${value as string}` : (value as string), 84 + ); 85 + url = url.replace(match, replaceValue); 86 + } 87 + } 88 + return url; 89 + }; 90 + 91 + export const createQuerySerializer = <T = unknown>({ 92 + allowReserved, 93 + array, 94 + object, 95 + }: QuerySerializerOptions = {}) => { 96 + const querySerializer = (queryParams: T) => { 97 + const search: string[] = []; 98 + if (queryParams && typeof queryParams === 'object') { 99 + for (const name in queryParams) { 100 + const value = queryParams[name]; 101 + 102 + if (value === undefined || value === null) { 103 + continue; 104 + } 105 + 106 + if (Array.isArray(value)) { 107 + const serializedArray = serializeArrayParam({ 108 + allowReserved, 109 + explode: true, 110 + name, 111 + style: 'form', 112 + value, 113 + ...array, 114 + }); 115 + if (serializedArray) search.push(serializedArray); 116 + } else if (typeof value === 'object') { 117 + const serializedObject = serializeObjectParam({ 118 + allowReserved, 119 + explode: true, 120 + name, 121 + style: 'deepObject', 122 + value: value as Record<string, unknown>, 123 + ...object, 124 + }); 125 + if (serializedObject) search.push(serializedObject); 126 + } else { 127 + const serializedPrimitive = serializePrimitiveParam({ 128 + allowReserved, 129 + name, 130 + value: value as string, 131 + }); 132 + if (serializedPrimitive) search.push(serializedPrimitive); 133 + } 134 + } 135 + } 136 + return search.join('&'); 137 + }; 138 + return querySerializer; 139 + }; 140 + 141 + export const setAuthParams = async ({ 142 + security, 143 + ...options 144 + }: Pick<Required<RequestOptions>, 'security'> & 145 + Pick<RequestOptions, 'auth' | 'query'> & { 146 + headers: Record<any, unknown>; 147 + }) => { 148 + for (const auth of security) { 149 + const token = await getAuthToken(auth, options.auth); 150 + 151 + if (!token) { 152 + continue; 153 + } 154 + 155 + const name = auth.name ?? 'Authorization'; 156 + 157 + switch (auth.in) { 158 + case 'query': 159 + if (!options.query) { 160 + options.query = {}; 161 + } 162 + options.query[name] = token; 163 + break; 164 + case 'cookie': { 165 + const value = `${name}=${token}`; 166 + if ('Cookie' in options.headers && options.headers['Cookie']) { 167 + options.headers['Cookie'] = `${options.headers['Cookie']}; ${value}`; 168 + } else { 169 + options.headers['Cookie'] = value; 170 + } 171 + break; 172 + } 173 + case 'header': 174 + default: 175 + options.headers[name] = token; 176 + break; 177 + } 178 + 179 + return; 180 + } 181 + }; 182 + 183 + export const buildUrl: Client['buildUrl'] = (options) => { 184 + const url = getUrl({ 185 + path: options.path, 186 + // let `paramsSerializer()` handle query params if it exists 187 + query: !options.paramsSerializer ? options.query : undefined, 188 + querySerializer: 189 + typeof options.querySerializer === 'function' 190 + ? options.querySerializer 191 + : createQuerySerializer(options.querySerializer), 192 + url: options.url, 193 + }); 194 + return url; 195 + }; 196 + 197 + export const getUrl = ({ 198 + path, 199 + query, 200 + querySerializer, 201 + url: _url, 202 + }: { 203 + path?: Record<string, unknown>; 204 + query?: Record<string, unknown>; 205 + querySerializer: QuerySerializer; 206 + url: string; 207 + }) => { 208 + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; 209 + let url = pathUrl; 210 + if (path) { 211 + url = defaultPathSerializer({ path, url }); 212 + } 213 + let search = query ? querySerializer(query) : ''; 214 + if (search.startsWith('?')) { 215 + search = search.substring(1); 216 + } 217 + if (search) { 218 + url += `?${search}`; 219 + } 220 + return url; 221 + }; 222 + 223 + export const mergeConfigs = (a: Config, b: Config): Config => { 224 + const config = { ...a, ...b }; 225 + config.headers = mergeHeaders(a.headers, b.headers); 226 + return config; 227 + }; 228 + 229 + /** 230 + * Special Axios headers keywords allowing to set headers by request method. 231 + */ 232 + export const axiosHeadersKeywords = [ 233 + 'common', 234 + 'delete', 235 + 'get', 236 + 'head', 237 + 'patch', 238 + 'post', 239 + 'put', 240 + ] as const; 241 + 242 + export const mergeHeaders = ( 243 + ...headers: Array<Required<Config>['headers'] | undefined> 244 + ): Record<any, unknown> => { 245 + const mergedHeaders: Record<any, unknown> = {}; 246 + for (const header of headers) { 247 + if (!header || typeof header !== 'object') { 248 + continue; 249 + } 250 + 251 + const iterator = Object.entries(header); 252 + 253 + for (const [key, value] of iterator) { 254 + if ( 255 + axiosHeadersKeywords.includes( 256 + key as (typeof axiosHeadersKeywords)[number], 257 + ) && 258 + typeof value === 'object' 259 + ) { 260 + mergedHeaders[key] = { 261 + ...(mergedHeaders[key] as Record<any, unknown>), 262 + ...value, 263 + }; 264 + } else if (value === null) { 265 + delete mergedHeaders[key]; 266 + } else if (Array.isArray(value)) { 267 + for (const v of value) { 268 + // @ts-expect-error 269 + mergedHeaders[key] = [...(mergedHeaders[key] ?? []), v as string]; 270 + } 271 + } else if (value !== undefined) { 272 + // assume object headers are meant to be JSON stringified, i.e. their 273 + // content value in OpenAPI specification is 'application/json' 274 + mergedHeaders[key] = 275 + typeof value === 'object' ? JSON.stringify(value) : (value as string); 276 + } 277 + } 278 + } 279 + return mergedHeaders; 280 + }; 281 + 282 + export const createConfig = <T extends ClientOptions = ClientOptions>( 283 + override: Config<Omit<ClientOptions, keyof T> & T> = {}, 284 + ): Config<Omit<ClientOptions, keyof T> & T> => ({ 285 + ...override, 286 + });
+40
examples/openapi-ts-axios/src/client/core/auth.ts
··· 1 + export type AuthToken = string | undefined; 2 + 3 + export interface Auth { 4 + /** 5 + * Which part of the request do we use to send the auth? 6 + * 7 + * @default 'header' 8 + */ 9 + in?: 'header' | 'query' | 'cookie'; 10 + /** 11 + * Header or query parameter name. 12 + * 13 + * @default 'Authorization' 14 + */ 15 + name?: string; 16 + scheme?: 'basic' | 'bearer'; 17 + type: 'apiKey' | 'http'; 18 + } 19 + 20 + export const getAuthToken = async ( 21 + auth: Auth, 22 + callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken, 23 + ): Promise<string | undefined> => { 24 + const token = 25 + typeof callback === 'function' ? await callback(auth) : callback; 26 + 27 + if (!token) { 28 + return; 29 + } 30 + 31 + if (auth.scheme === 'bearer') { 32 + return `Bearer ${token}`; 33 + } 34 + 35 + if (auth.scheme === 'basic') { 36 + return `Basic ${btoa(token)}`; 37 + } 38 + 39 + return token; 40 + };
+84
examples/openapi-ts-axios/src/client/core/bodySerializer.ts
··· 1 + import type { 2 + ArrayStyle, 3 + ObjectStyle, 4 + SerializerOptions, 5 + } from './pathSerializer'; 6 + 7 + export type QuerySerializer = (query: Record<string, unknown>) => string; 8 + 9 + export type BodySerializer = (body: any) => any; 10 + 11 + export interface QuerySerializerOptions { 12 + allowReserved?: boolean; 13 + array?: SerializerOptions<ArrayStyle>; 14 + object?: SerializerOptions<ObjectStyle>; 15 + } 16 + 17 + const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { 18 + if (typeof value === 'string' || value instanceof Blob) { 19 + data.append(key, value); 20 + } else { 21 + data.append(key, JSON.stringify(value)); 22 + } 23 + }; 24 + 25 + const serializeUrlSearchParamsPair = ( 26 + data: URLSearchParams, 27 + key: string, 28 + value: unknown, 29 + ) => { 30 + if (typeof value === 'string') { 31 + data.append(key, value); 32 + } else { 33 + data.append(key, JSON.stringify(value)); 34 + } 35 + }; 36 + 37 + export const formDataBodySerializer = { 38 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 39 + body: T, 40 + ) => { 41 + const data = new FormData(); 42 + 43 + Object.entries(body).forEach(([key, value]) => { 44 + if (value === undefined || value === null) { 45 + return; 46 + } 47 + if (Array.isArray(value)) { 48 + value.forEach((v) => serializeFormDataPair(data, key, v)); 49 + } else { 50 + serializeFormDataPair(data, key, value); 51 + } 52 + }); 53 + 54 + return data; 55 + }, 56 + }; 57 + 58 + export const jsonBodySerializer = { 59 + bodySerializer: <T>(body: T) => 60 + JSON.stringify(body, (key, value) => 61 + typeof value === 'bigint' ? value.toString() : value, 62 + ), 63 + }; 64 + 65 + export const urlSearchParamsBodySerializer = { 66 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 67 + body: T, 68 + ) => { 69 + const data = new URLSearchParams(); 70 + 71 + Object.entries(body).forEach(([key, value]) => { 72 + if (value === undefined || value === null) { 73 + return; 74 + } 75 + if (Array.isArray(value)) { 76 + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); 77 + } else { 78 + serializeUrlSearchParamsPair(data, key, value); 79 + } 80 + }); 81 + 82 + return data.toString(); 83 + }, 84 + };
+141
examples/openapi-ts-axios/src/client/core/params.ts
··· 1 + type Slot = 'body' | 'headers' | 'path' | 'query'; 2 + 3 + export type Field = 4 + | { 5 + in: Exclude<Slot, 'body'>; 6 + key: string; 7 + map?: string; 8 + } 9 + | { 10 + in: Extract<Slot, 'body'>; 11 + key?: string; 12 + map?: string; 13 + }; 14 + 15 + export interface Fields { 16 + allowExtra?: Partial<Record<Slot, boolean>>; 17 + args?: ReadonlyArray<Field>; 18 + } 19 + 20 + export type FieldsConfig = ReadonlyArray<Field | Fields>; 21 + 22 + const extraPrefixesMap: Record<string, Slot> = { 23 + $body_: 'body', 24 + $headers_: 'headers', 25 + $path_: 'path', 26 + $query_: 'query', 27 + }; 28 + const extraPrefixes = Object.entries(extraPrefixesMap); 29 + 30 + type KeyMap = Map< 31 + string, 32 + { 33 + in: Slot; 34 + map?: string; 35 + } 36 + >; 37 + 38 + const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { 39 + if (!map) { 40 + map = new Map(); 41 + } 42 + 43 + for (const config of fields) { 44 + if ('in' in config) { 45 + if (config.key) { 46 + map.set(config.key, { 47 + in: config.in, 48 + map: config.map, 49 + }); 50 + } 51 + } else if (config.args) { 52 + buildKeyMap(config.args, map); 53 + } 54 + } 55 + 56 + return map; 57 + }; 58 + 59 + interface Params { 60 + body: unknown; 61 + headers: Record<string, unknown>; 62 + path: Record<string, unknown>; 63 + query: Record<string, unknown>; 64 + } 65 + 66 + const stripEmptySlots = (params: Params) => { 67 + for (const [slot, value] of Object.entries(params)) { 68 + if (value && typeof value === 'object' && !Object.keys(value).length) { 69 + delete params[slot as Slot]; 70 + } 71 + } 72 + }; 73 + 74 + export const buildClientParams = ( 75 + args: ReadonlyArray<unknown>, 76 + fields: FieldsConfig, 77 + ) => { 78 + const params: Params = { 79 + body: {}, 80 + headers: {}, 81 + path: {}, 82 + query: {}, 83 + }; 84 + 85 + const map = buildKeyMap(fields); 86 + 87 + let config: FieldsConfig[number] | undefined; 88 + 89 + for (const [index, arg] of args.entries()) { 90 + if (fields[index]) { 91 + config = fields[index]; 92 + } 93 + 94 + if (!config) { 95 + continue; 96 + } 97 + 98 + if ('in' in config) { 99 + if (config.key) { 100 + const field = map.get(config.key)!; 101 + const name = field.map || config.key; 102 + (params[field.in] as Record<string, unknown>)[name] = arg; 103 + } else { 104 + params.body = arg; 105 + } 106 + } else { 107 + for (const [key, value] of Object.entries(arg ?? {})) { 108 + const field = map.get(key); 109 + 110 + if (field) { 111 + const name = field.map || key; 112 + (params[field.in] as Record<string, unknown>)[name] = value; 113 + } else { 114 + const extra = extraPrefixes.find(([prefix]) => 115 + key.startsWith(prefix), 116 + ); 117 + 118 + if (extra) { 119 + const [prefix, slot] = extra; 120 + (params[slot] as Record<string, unknown>)[ 121 + key.slice(prefix.length) 122 + ] = value; 123 + } else { 124 + for (const [slot, allowed] of Object.entries( 125 + config.allowExtra ?? {}, 126 + )) { 127 + if (allowed) { 128 + (params[slot as Slot] as Record<string, unknown>)[key] = value; 129 + break; 130 + } 131 + } 132 + } 133 + } 134 + } 135 + } 136 + } 137 + 138 + stripEmptySlots(params); 139 + 140 + return params; 141 + };
+179
examples/openapi-ts-axios/src/client/core/pathSerializer.ts
··· 1 + interface SerializeOptions<T> 2 + extends SerializePrimitiveOptions, 3 + SerializerOptions<T> {} 4 + 5 + interface SerializePrimitiveOptions { 6 + allowReserved?: boolean; 7 + name: string; 8 + } 9 + 10 + export interface SerializerOptions<T> { 11 + /** 12 + * @default true 13 + */ 14 + explode: boolean; 15 + style: T; 16 + } 17 + 18 + export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; 19 + export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 20 + type MatrixStyle = 'label' | 'matrix' | 'simple'; 21 + export type ObjectStyle = 'form' | 'deepObject'; 22 + type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; 23 + 24 + interface SerializePrimitiveParam extends SerializePrimitiveOptions { 25 + value: string; 26 + } 27 + 28 + export const separatorArrayExplode = (style: ArraySeparatorStyle) => { 29 + switch (style) { 30 + case 'label': 31 + return '.'; 32 + case 'matrix': 33 + return ';'; 34 + case 'simple': 35 + return ','; 36 + default: 37 + return '&'; 38 + } 39 + }; 40 + 41 + export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { 42 + switch (style) { 43 + case 'form': 44 + return ','; 45 + case 'pipeDelimited': 46 + return '|'; 47 + case 'spaceDelimited': 48 + return '%20'; 49 + default: 50 + return ','; 51 + } 52 + }; 53 + 54 + export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { 55 + switch (style) { 56 + case 'label': 57 + return '.'; 58 + case 'matrix': 59 + return ';'; 60 + case 'simple': 61 + return ','; 62 + default: 63 + return '&'; 64 + } 65 + }; 66 + 67 + export const serializeArrayParam = ({ 68 + allowReserved, 69 + explode, 70 + name, 71 + style, 72 + value, 73 + }: SerializeOptions<ArraySeparatorStyle> & { 74 + value: unknown[]; 75 + }) => { 76 + if (!explode) { 77 + const joinedValues = ( 78 + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) 79 + ).join(separatorArrayNoExplode(style)); 80 + switch (style) { 81 + case 'label': 82 + return `.${joinedValues}`; 83 + case 'matrix': 84 + return `;${name}=${joinedValues}`; 85 + case 'simple': 86 + return joinedValues; 87 + default: 88 + return `${name}=${joinedValues}`; 89 + } 90 + } 91 + 92 + const separator = separatorArrayExplode(style); 93 + const joinedValues = value 94 + .map((v) => { 95 + if (style === 'label' || style === 'simple') { 96 + return allowReserved ? v : encodeURIComponent(v as string); 97 + } 98 + 99 + return serializePrimitiveParam({ 100 + allowReserved, 101 + name, 102 + value: v as string, 103 + }); 104 + }) 105 + .join(separator); 106 + return style === 'label' || style === 'matrix' 107 + ? separator + joinedValues 108 + : joinedValues; 109 + }; 110 + 111 + export const serializePrimitiveParam = ({ 112 + allowReserved, 113 + name, 114 + value, 115 + }: SerializePrimitiveParam) => { 116 + if (value === undefined || value === null) { 117 + return ''; 118 + } 119 + 120 + if (typeof value === 'object') { 121 + throw new Error( 122 + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', 123 + ); 124 + } 125 + 126 + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; 127 + }; 128 + 129 + export const serializeObjectParam = ({ 130 + allowReserved, 131 + explode, 132 + name, 133 + style, 134 + value, 135 + valueOnly, 136 + }: SerializeOptions<ObjectSeparatorStyle> & { 137 + value: Record<string, unknown> | Date; 138 + valueOnly?: boolean; 139 + }) => { 140 + if (value instanceof Date) { 141 + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; 142 + } 143 + 144 + if (style !== 'deepObject' && !explode) { 145 + let values: string[] = []; 146 + Object.entries(value).forEach(([key, v]) => { 147 + values = [ 148 + ...values, 149 + key, 150 + allowReserved ? (v as string) : encodeURIComponent(v as string), 151 + ]; 152 + }); 153 + const joinedValues = values.join(','); 154 + switch (style) { 155 + case 'form': 156 + return `${name}=${joinedValues}`; 157 + case 'label': 158 + return `.${joinedValues}`; 159 + case 'matrix': 160 + return `;${name}=${joinedValues}`; 161 + default: 162 + return joinedValues; 163 + } 164 + } 165 + 166 + const separator = separatorObjectExplode(style); 167 + const joinedValues = Object.entries(value) 168 + .map(([key, v]) => 169 + serializePrimitiveParam({ 170 + allowReserved, 171 + name: style === 'deepObject' ? `${name}[${key}]` : key, 172 + value: v as string, 173 + }), 174 + ) 175 + .join(separator); 176 + return style === 'label' || style === 'matrix' 177 + ? separator + joinedValues 178 + : joinedValues; 179 + };
+98
examples/openapi-ts-axios/src/client/core/types.ts
··· 1 + import type { Auth, AuthToken } from './auth'; 2 + import type { 3 + BodySerializer, 4 + QuerySerializer, 5 + QuerySerializerOptions, 6 + } from './bodySerializer'; 7 + 8 + export interface Client< 9 + RequestFn = never, 10 + Config = unknown, 11 + MethodFn = never, 12 + BuildUrlFn = never, 13 + > { 14 + /** 15 + * Returns the final request URL. 16 + */ 17 + buildUrl: BuildUrlFn; 18 + connect: MethodFn; 19 + delete: MethodFn; 20 + get: MethodFn; 21 + getConfig: () => Config; 22 + head: MethodFn; 23 + options: MethodFn; 24 + patch: MethodFn; 25 + post: MethodFn; 26 + put: MethodFn; 27 + request: RequestFn; 28 + setConfig: (config: Config) => Config; 29 + trace: MethodFn; 30 + } 31 + 32 + export interface Config { 33 + /** 34 + * Auth token or a function returning auth token. The resolved value will be 35 + * added to the request payload as defined by its `security` array. 36 + */ 37 + auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken; 38 + /** 39 + * A function for serializing request body parameter. By default, 40 + * {@link JSON.stringify()} will be used. 41 + */ 42 + bodySerializer?: BodySerializer | null; 43 + /** 44 + * An object containing any HTTP headers that you want to pre-populate your 45 + * `Headers` object with. 46 + * 47 + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} 48 + */ 49 + headers?: 50 + | RequestInit['headers'] 51 + | Record< 52 + string, 53 + | string 54 + | number 55 + | boolean 56 + | (string | number | boolean)[] 57 + | null 58 + | undefined 59 + | unknown 60 + >; 61 + /** 62 + * The request method. 63 + * 64 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} 65 + */ 66 + method?: 67 + | 'CONNECT' 68 + | 'DELETE' 69 + | 'GET' 70 + | 'HEAD' 71 + | 'OPTIONS' 72 + | 'PATCH' 73 + | 'POST' 74 + | 'PUT' 75 + | 'TRACE'; 76 + /** 77 + * A function for serializing request query parameters. By default, arrays 78 + * will be exploded in form style, objects will be exploded in deepObject 79 + * style, and reserved characters are percent-encoded. 80 + * 81 + * This method will have no effect if the native `paramsSerializer()` Axios 82 + * API function is used. 83 + * 84 + * {@link https://swagger.io/docs/specification/serialization/#query View examples} 85 + */ 86 + querySerializer?: QuerySerializer | QuerySerializerOptions; 87 + /** 88 + * A function transforming response data before it's returned. This is useful 89 + * for post-processing data, e.g. converting ISO strings into Date objects. 90 + */ 91 + responseTransformer?: (data: unknown) => Promise<unknown>; 92 + /** 93 + * A function validating response data. This is useful if you want to ensure 94 + * the response conforms to the desired shape, so it can be safely passed to 95 + * the transformers and returned to the user. 96 + */ 97 + responseValidator?: (data: unknown) => Promise<unknown>; 98 + }
+5 -10
examples/openapi-ts-axios/src/client/sdk.gen.ts
··· 1 1 // This file is auto-generated by @hey-api/openapi-ts 2 2 3 - import type { 4 - Client, 5 - Options as ClientOptions, 6 - TDataShape, 7 - } from '@hey-api/client-axios'; 8 - 3 + import type { Client, Options as ClientOptions, TDataShape } from './client'; 9 4 import { client as _heyApiClient } from './client.gen'; 10 5 import type { 11 6 AddPetData, ··· 107 102 ...options, 108 103 headers: { 109 104 'Content-Type': 'application/json', 110 - ...options?.headers, 105 + ...options.headers, 111 106 }, 112 107 }); 113 108 ··· 134 129 ...options, 135 130 headers: { 136 131 'Content-Type': 'application/json', 137 - ...options?.headers, 132 + ...options.headers, 138 133 }, 139 134 }); 140 135 ··· 280 275 ...options, 281 276 headers: { 282 277 'Content-Type': 'application/octet-stream', 283 - ...options?.headers, 278 + ...options.headers, 284 279 }, 285 280 }); 286 281 ··· 485 480 ...options, 486 481 headers: { 487 482 'Content-Type': 'application/json', 488 - ...options?.headers, 483 + ...options.headers, 489 484 }, 490 485 });
+1 -1
examples/openapi-ts-fastify/openapi-ts.config.ts
··· 8 8 lint: 'eslint', 9 9 path: './src/client', 10 10 }, 11 - plugins: ['@hey-api/client-fetch', 'fastify', '@hey-api/sdk'], 11 + plugins: ['fastify', '@hey-api/sdk'], 12 12 });
-1
examples/openapi-ts-fastify/package.json
··· 9 9 "typecheck": "tsc --noEmit" 10 10 }, 11 11 "dependencies": { 12 - "@hey-api/client-fetch": "workspace:*", 13 12 "fastify": "5.2.0", 14 13 "fastify-openapi-glue": "4.8.0" 15 14 },
+1 -2
examples/openapi-ts-fastify/src/client/client.gen.ts
··· 5 5 type Config, 6 6 createClient, 7 7 createConfig, 8 - } from '@hey-api/client-fetch'; 9 - 8 + } from './client'; 10 9 import type { ClientOptions } from './types.gen'; 11 10 12 11 /**
+181
examples/openapi-ts-fastify/src/client/client/client.ts
··· 1 + import type { Client, Config, RequestOptions } from './types'; 2 + import { 3 + buildUrl, 4 + createConfig, 5 + createInterceptors, 6 + getParseAs, 7 + mergeConfigs, 8 + mergeHeaders, 9 + setAuthParams, 10 + } from './utils'; 11 + 12 + type ReqInit = Omit<RequestInit, 'body' | 'headers'> & { 13 + body?: any; 14 + headers: ReturnType<typeof mergeHeaders>; 15 + }; 16 + 17 + export const createClient = (config: Config = {}): Client => { 18 + let _config = mergeConfigs(createConfig(), config); 19 + 20 + const getConfig = (): Config => ({ ..._config }); 21 + 22 + const setConfig = (config: Config): Config => { 23 + _config = mergeConfigs(_config, config); 24 + return getConfig(); 25 + }; 26 + 27 + const interceptors = createInterceptors< 28 + Request, 29 + Response, 30 + unknown, 31 + RequestOptions 32 + >(); 33 + 34 + const request: Client['request'] = async (options) => { 35 + const opts = { 36 + ..._config, 37 + ...options, 38 + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, 39 + headers: mergeHeaders(_config.headers, options.headers), 40 + }; 41 + 42 + if (opts.security) { 43 + await setAuthParams({ 44 + ...opts, 45 + security: opts.security, 46 + }); 47 + } 48 + 49 + if (opts.body && opts.bodySerializer) { 50 + opts.body = opts.bodySerializer(opts.body); 51 + } 52 + 53 + // remove Content-Type header if body is empty to avoid sending invalid requests 54 + if (opts.body === undefined || opts.body === '') { 55 + opts.headers.delete('Content-Type'); 56 + } 57 + 58 + const url = buildUrl(opts); 59 + const requestInit: ReqInit = { 60 + redirect: 'follow', 61 + ...opts, 62 + }; 63 + 64 + let request = new Request(url, requestInit); 65 + 66 + for (const fn of interceptors.request._fns) { 67 + if (fn) { 68 + request = await fn(request, opts); 69 + } 70 + } 71 + 72 + // fetch must be assigned here, otherwise it would throw the error: 73 + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation 74 + const _fetch = opts.fetch!; 75 + let response = await _fetch(request); 76 + 77 + for (const fn of interceptors.response._fns) { 78 + if (fn) { 79 + response = await fn(response, request, opts); 80 + } 81 + } 82 + 83 + const result = { 84 + request, 85 + response, 86 + }; 87 + 88 + if (response.ok) { 89 + if ( 90 + response.status === 204 || 91 + response.headers.get('Content-Length') === '0' 92 + ) { 93 + return opts.responseStyle === 'data' 94 + ? {} 95 + : { 96 + data: {}, 97 + ...result, 98 + }; 99 + } 100 + 101 + const parseAs = 102 + (opts.parseAs === 'auto' 103 + ? getParseAs(response.headers.get('Content-Type')) 104 + : opts.parseAs) ?? 'json'; 105 + 106 + if (parseAs === 'stream') { 107 + return opts.responseStyle === 'data' 108 + ? response.body 109 + : { 110 + data: response.body, 111 + ...result, 112 + }; 113 + } 114 + 115 + let data = await response[parseAs](); 116 + if (parseAs === 'json') { 117 + if (opts.responseValidator) { 118 + await opts.responseValidator(data); 119 + } 120 + 121 + if (opts.responseTransformer) { 122 + data = await opts.responseTransformer(data); 123 + } 124 + } 125 + 126 + return opts.responseStyle === 'data' 127 + ? data 128 + : { 129 + data, 130 + ...result, 131 + }; 132 + } 133 + 134 + let error = await response.text(); 135 + 136 + try { 137 + error = JSON.parse(error); 138 + } catch { 139 + // noop 140 + } 141 + 142 + let finalError = error; 143 + 144 + for (const fn of interceptors.error._fns) { 145 + if (fn) { 146 + finalError = (await fn(error, response, request, opts)) as string; 147 + } 148 + } 149 + 150 + finalError = finalError || ({} as string); 151 + 152 + if (opts.throwOnError) { 153 + throw finalError; 154 + } 155 + 156 + // TODO: we probably want to return error and improve types 157 + return opts.responseStyle === 'data' 158 + ? undefined 159 + : { 160 + error: finalError, 161 + ...result, 162 + }; 163 + }; 164 + 165 + return { 166 + buildUrl, 167 + connect: (options) => request({ ...options, method: 'CONNECT' }), 168 + delete: (options) => request({ ...options, method: 'DELETE' }), 169 + get: (options) => request({ ...options, method: 'GET' }), 170 + getConfig, 171 + head: (options) => request({ ...options, method: 'HEAD' }), 172 + interceptors, 173 + options: (options) => request({ ...options, method: 'OPTIONS' }), 174 + patch: (options) => request({ ...options, method: 'PATCH' }), 175 + post: (options) => request({ ...options, method: 'POST' }), 176 + put: (options) => request({ ...options, method: 'PUT' }), 177 + request, 178 + setConfig, 179 + trace: (options) => request({ ...options, method: 'TRACE' }), 180 + }; 181 + };
+22
examples/openapi-ts-fastify/src/client/client/index.ts
··· 1 + export type { Auth } from '../core/auth'; 2 + export type { QuerySerializerOptions } from '../core/bodySerializer'; 3 + export { 4 + formDataBodySerializer, 5 + jsonBodySerializer, 6 + urlSearchParamsBodySerializer, 7 + } from '../core/bodySerializer'; 8 + export { buildClientParams } from '../core/params'; 9 + export { createClient } from './client'; 10 + export type { 11 + Client, 12 + ClientOptions, 13 + Config, 14 + CreateClientConfig, 15 + Options, 16 + OptionsLegacyParser, 17 + RequestOptions, 18 + RequestResult, 19 + ResponseStyle, 20 + TDataShape, 21 + } from './types'; 22 + export { createConfig, mergeHeaders } from './utils';
+212
examples/openapi-ts-fastify/src/client/client/types.ts
··· 1 + import type { Auth } from '../core/auth'; 2 + import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; 3 + import type { Middleware } from './utils'; 4 + 5 + export type ResponseStyle = 'data' | 'fields'; 6 + 7 + export interface Config<T extends ClientOptions = ClientOptions> 8 + extends Omit<RequestInit, 'body' | 'headers' | 'method'>, 9 + CoreConfig { 10 + /** 11 + * Base URL for all requests made by this client. 12 + */ 13 + baseUrl?: T['baseUrl']; 14 + /** 15 + * Fetch API implementation. You can use this option to provide a custom 16 + * fetch instance. 17 + * 18 + * @default globalThis.fetch 19 + */ 20 + fetch?: (request: Request) => ReturnType<typeof fetch>; 21 + /** 22 + * Please don't use the Fetch client for Next.js applications. The `next` 23 + * options won't have any effect. 24 + * 25 + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. 26 + */ 27 + next?: never; 28 + /** 29 + * Return the response data parsed in a specified format. By default, `auto` 30 + * will infer the appropriate method from the `Content-Type` response header. 31 + * You can override this behavior with any of the {@link Body} methods. 32 + * Select `stream` if you don't want to parse response data at all. 33 + * 34 + * @default 'auto' 35 + */ 36 + parseAs?: Exclude<keyof Body, 'body' | 'bodyUsed'> | 'auto' | 'stream'; 37 + /** 38 + * Should we return only data or multiple fields (data, error, response, etc.)? 39 + * 40 + * @default 'fields' 41 + */ 42 + responseStyle?: ResponseStyle; 43 + /** 44 + * Throw an error instead of returning it in the response? 45 + * 46 + * @default false 47 + */ 48 + throwOnError?: T['throwOnError']; 49 + } 50 + 51 + export interface RequestOptions< 52 + TResponseStyle extends ResponseStyle = 'fields', 53 + ThrowOnError extends boolean = boolean, 54 + Url extends string = string, 55 + > extends Config<{ 56 + responseStyle: TResponseStyle; 57 + throwOnError: ThrowOnError; 58 + }> { 59 + /** 60 + * Any body that you want to add to your request. 61 + * 62 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} 63 + */ 64 + body?: unknown; 65 + path?: Record<string, unknown>; 66 + query?: Record<string, unknown>; 67 + /** 68 + * Security mechanism(s) to use for the request. 69 + */ 70 + security?: ReadonlyArray<Auth>; 71 + url: Url; 72 + } 73 + 74 + export type RequestResult< 75 + TData = unknown, 76 + TError = unknown, 77 + ThrowOnError extends boolean = boolean, 78 + TResponseStyle extends ResponseStyle = 'fields', 79 + > = ThrowOnError extends true 80 + ? Promise< 81 + TResponseStyle extends 'data' 82 + ? TData extends Record<string, unknown> 83 + ? TData[keyof TData] 84 + : TData 85 + : { 86 + data: TData extends Record<string, unknown> 87 + ? TData[keyof TData] 88 + : TData; 89 + request: Request; 90 + response: Response; 91 + } 92 + > 93 + : Promise< 94 + TResponseStyle extends 'data' 95 + ? 96 + | (TData extends Record<string, unknown> 97 + ? TData[keyof TData] 98 + : TData) 99 + | undefined 100 + : ( 101 + | { 102 + data: TData extends Record<string, unknown> 103 + ? TData[keyof TData] 104 + : TData; 105 + error: undefined; 106 + } 107 + | { 108 + data: undefined; 109 + error: TError extends Record<string, unknown> 110 + ? TError[keyof TError] 111 + : TError; 112 + } 113 + ) & { 114 + request: Request; 115 + response: Response; 116 + } 117 + >; 118 + 119 + export interface ClientOptions { 120 + baseUrl?: string; 121 + responseStyle?: ResponseStyle; 122 + throwOnError?: boolean; 123 + } 124 + 125 + type MethodFn = < 126 + TData = unknown, 127 + TError = unknown, 128 + ThrowOnError extends boolean = false, 129 + TResponseStyle extends ResponseStyle = 'fields', 130 + >( 131 + options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'>, 132 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 133 + 134 + type RequestFn = < 135 + TData = unknown, 136 + TError = unknown, 137 + ThrowOnError extends boolean = false, 138 + TResponseStyle extends ResponseStyle = 'fields', 139 + >( 140 + options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'> & 141 + Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, 'method'>, 142 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 143 + 144 + type BuildUrlFn = < 145 + TData extends { 146 + body?: unknown; 147 + path?: Record<string, unknown>; 148 + query?: Record<string, unknown>; 149 + url: string; 150 + }, 151 + >( 152 + options: Pick<TData, 'url'> & Options<TData>, 153 + ) => string; 154 + 155 + export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & { 156 + interceptors: Middleware<Request, Response, unknown, RequestOptions>; 157 + }; 158 + 159 + /** 160 + * The `createClientConfig()` function will be called on client initialization 161 + * and the returned object will become the client's initial configuration. 162 + * 163 + * You may want to initialize your client this way instead of calling 164 + * `setConfig()`. This is useful for example if you're using Next.js 165 + * to ensure your client always has the correct values. 166 + */ 167 + export type CreateClientConfig<T extends ClientOptions = ClientOptions> = ( 168 + override?: Config<ClientOptions & T>, 169 + ) => Config<Required<ClientOptions> & T>; 170 + 171 + export interface TDataShape { 172 + body?: unknown; 173 + headers?: unknown; 174 + path?: unknown; 175 + query?: unknown; 176 + url: string; 177 + } 178 + 179 + type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>; 180 + 181 + export type Options< 182 + TData extends TDataShape = TDataShape, 183 + ThrowOnError extends boolean = boolean, 184 + TResponseStyle extends ResponseStyle = 'fields', 185 + > = OmitKeys< 186 + RequestOptions<TResponseStyle, ThrowOnError>, 187 + 'body' | 'path' | 'query' | 'url' 188 + > & 189 + Omit<TData, 'url'>; 190 + 191 + export type OptionsLegacyParser< 192 + TData = unknown, 193 + ThrowOnError extends boolean = boolean, 194 + TResponseStyle extends ResponseStyle = 'fields', 195 + > = TData extends { body?: any } 196 + ? TData extends { headers?: any } 197 + ? OmitKeys< 198 + RequestOptions<TResponseStyle, ThrowOnError>, 199 + 'body' | 'headers' | 'url' 200 + > & 201 + TData 202 + : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'body' | 'url'> & 203 + TData & 204 + Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'headers'> 205 + : TData extends { headers?: any } 206 + ? OmitKeys< 207 + RequestOptions<TResponseStyle, ThrowOnError>, 208 + 'headers' | 'url' 209 + > & 210 + TData & 211 + Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'body'> 212 + : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'url'> & TData;
+415
examples/openapi-ts-fastify/src/client/client/utils.ts
··· 1 + import { getAuthToken } from '../core/auth'; 2 + import type { 3 + QuerySerializer, 4 + QuerySerializerOptions, 5 + } from '../core/bodySerializer'; 6 + import { jsonBodySerializer } from '../core/bodySerializer'; 7 + import { 8 + serializeArrayParam, 9 + serializeObjectParam, 10 + serializePrimitiveParam, 11 + } from '../core/pathSerializer'; 12 + import type { Client, ClientOptions, Config, RequestOptions } from './types'; 13 + 14 + interface PathSerializer { 15 + path: Record<string, unknown>; 16 + url: string; 17 + } 18 + 19 + const PATH_PARAM_RE = /\{[^{}]+\}/g; 20 + 21 + type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; 22 + type MatrixStyle = 'label' | 'matrix' | 'simple'; 23 + type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 24 + 25 + const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { 26 + let url = _url; 27 + const matches = _url.match(PATH_PARAM_RE); 28 + if (matches) { 29 + for (const match of matches) { 30 + let explode = false; 31 + let name = match.substring(1, match.length - 1); 32 + let style: ArraySeparatorStyle = 'simple'; 33 + 34 + if (name.endsWith('*')) { 35 + explode = true; 36 + name = name.substring(0, name.length - 1); 37 + } 38 + 39 + if (name.startsWith('.')) { 40 + name = name.substring(1); 41 + style = 'label'; 42 + } else if (name.startsWith(';')) { 43 + name = name.substring(1); 44 + style = 'matrix'; 45 + } 46 + 47 + const value = path[name]; 48 + 49 + if (value === undefined || value === null) { 50 + continue; 51 + } 52 + 53 + if (Array.isArray(value)) { 54 + url = url.replace( 55 + match, 56 + serializeArrayParam({ explode, name, style, value }), 57 + ); 58 + continue; 59 + } 60 + 61 + if (typeof value === 'object') { 62 + url = url.replace( 63 + match, 64 + serializeObjectParam({ 65 + explode, 66 + name, 67 + style, 68 + value: value as Record<string, unknown>, 69 + valueOnly: true, 70 + }), 71 + ); 72 + continue; 73 + } 74 + 75 + if (style === 'matrix') { 76 + url = url.replace( 77 + match, 78 + `;${serializePrimitiveParam({ 79 + name, 80 + value: value as string, 81 + })}`, 82 + ); 83 + continue; 84 + } 85 + 86 + const replaceValue = encodeURIComponent( 87 + style === 'label' ? `.${value as string}` : (value as string), 88 + ); 89 + url = url.replace(match, replaceValue); 90 + } 91 + } 92 + return url; 93 + }; 94 + 95 + export const createQuerySerializer = <T = unknown>({ 96 + allowReserved, 97 + array, 98 + object, 99 + }: QuerySerializerOptions = {}) => { 100 + const querySerializer = (queryParams: T) => { 101 + const search: string[] = []; 102 + if (queryParams && typeof queryParams === 'object') { 103 + for (const name in queryParams) { 104 + const value = queryParams[name]; 105 + 106 + if (value === undefined || value === null) { 107 + continue; 108 + } 109 + 110 + if (Array.isArray(value)) { 111 + const serializedArray = serializeArrayParam({ 112 + allowReserved, 113 + explode: true, 114 + name, 115 + style: 'form', 116 + value, 117 + ...array, 118 + }); 119 + if (serializedArray) search.push(serializedArray); 120 + } else if (typeof value === 'object') { 121 + const serializedObject = serializeObjectParam({ 122 + allowReserved, 123 + explode: true, 124 + name, 125 + style: 'deepObject', 126 + value: value as Record<string, unknown>, 127 + ...object, 128 + }); 129 + if (serializedObject) search.push(serializedObject); 130 + } else { 131 + const serializedPrimitive = serializePrimitiveParam({ 132 + allowReserved, 133 + name, 134 + value: value as string, 135 + }); 136 + if (serializedPrimitive) search.push(serializedPrimitive); 137 + } 138 + } 139 + } 140 + return search.join('&'); 141 + }; 142 + return querySerializer; 143 + }; 144 + 145 + /** 146 + * Infers parseAs value from provided Content-Type header. 147 + */ 148 + export const getParseAs = ( 149 + contentType: string | null, 150 + ): Exclude<Config['parseAs'], 'auto'> => { 151 + if (!contentType) { 152 + // If no Content-Type header is provided, the best we can do is return the raw response body, 153 + // which is effectively the same as the 'stream' option. 154 + return 'stream'; 155 + } 156 + 157 + const cleanContent = contentType.split(';')[0]?.trim(); 158 + 159 + if (!cleanContent) { 160 + return; 161 + } 162 + 163 + if ( 164 + cleanContent.startsWith('application/json') || 165 + cleanContent.endsWith('+json') 166 + ) { 167 + return 'json'; 168 + } 169 + 170 + if (cleanContent === 'multipart/form-data') { 171 + return 'formData'; 172 + } 173 + 174 + if ( 175 + ['application/', 'audio/', 'image/', 'video/'].some((type) => 176 + cleanContent.startsWith(type), 177 + ) 178 + ) { 179 + return 'blob'; 180 + } 181 + 182 + if (cleanContent.startsWith('text/')) { 183 + return 'text'; 184 + } 185 + }; 186 + 187 + export const setAuthParams = async ({ 188 + security, 189 + ...options 190 + }: Pick<Required<RequestOptions>, 'security'> & 191 + Pick<RequestOptions, 'auth' | 'query'> & { 192 + headers: Headers; 193 + }) => { 194 + for (const auth of security) { 195 + const token = await getAuthToken(auth, options.auth); 196 + 197 + if (!token) { 198 + continue; 199 + } 200 + 201 + const name = auth.name ?? 'Authorization'; 202 + 203 + switch (auth.in) { 204 + case 'query': 205 + if (!options.query) { 206 + options.query = {}; 207 + } 208 + options.query[name] = token; 209 + break; 210 + case 'cookie': 211 + options.headers.append('Cookie', `${name}=${token}`); 212 + break; 213 + case 'header': 214 + default: 215 + options.headers.set(name, token); 216 + break; 217 + } 218 + 219 + return; 220 + } 221 + }; 222 + 223 + export const buildUrl: Client['buildUrl'] = (options) => { 224 + const url = getUrl({ 225 + baseUrl: options.baseUrl as string, 226 + path: options.path, 227 + query: options.query, 228 + querySerializer: 229 + typeof options.querySerializer === 'function' 230 + ? options.querySerializer 231 + : createQuerySerializer(options.querySerializer), 232 + url: options.url, 233 + }); 234 + return url; 235 + }; 236 + 237 + export const getUrl = ({ 238 + baseUrl, 239 + path, 240 + query, 241 + querySerializer, 242 + url: _url, 243 + }: { 244 + baseUrl?: string; 245 + path?: Record<string, unknown>; 246 + query?: Record<string, unknown>; 247 + querySerializer: QuerySerializer; 248 + url: string; 249 + }) => { 250 + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; 251 + let url = (baseUrl ?? '') + pathUrl; 252 + if (path) { 253 + url = defaultPathSerializer({ path, url }); 254 + } 255 + let search = query ? querySerializer(query) : ''; 256 + if (search.startsWith('?')) { 257 + search = search.substring(1); 258 + } 259 + if (search) { 260 + url += `?${search}`; 261 + } 262 + return url; 263 + }; 264 + 265 + export const mergeConfigs = (a: Config, b: Config): Config => { 266 + const config = { ...a, ...b }; 267 + if (config.baseUrl?.endsWith('/')) { 268 + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); 269 + } 270 + config.headers = mergeHeaders(a.headers, b.headers); 271 + return config; 272 + }; 273 + 274 + export const mergeHeaders = ( 275 + ...headers: Array<Required<Config>['headers'] | undefined> 276 + ): Headers => { 277 + const mergedHeaders = new Headers(); 278 + for (const header of headers) { 279 + if (!header || typeof header !== 'object') { 280 + continue; 281 + } 282 + 283 + const iterator = 284 + header instanceof Headers ? header.entries() : Object.entries(header); 285 + 286 + for (const [key, value] of iterator) { 287 + if (value === null) { 288 + mergedHeaders.delete(key); 289 + } else if (Array.isArray(value)) { 290 + for (const v of value) { 291 + mergedHeaders.append(key, v as string); 292 + } 293 + } else if (value !== undefined) { 294 + // assume object headers are meant to be JSON stringified, i.e. their 295 + // content value in OpenAPI specification is 'application/json' 296 + mergedHeaders.set( 297 + key, 298 + typeof value === 'object' ? JSON.stringify(value) : (value as string), 299 + ); 300 + } 301 + } 302 + } 303 + return mergedHeaders; 304 + }; 305 + 306 + type ErrInterceptor<Err, Res, Req, Options> = ( 307 + error: Err, 308 + response: Res, 309 + request: Req, 310 + options: Options, 311 + ) => Err | Promise<Err>; 312 + 313 + type ReqInterceptor<Req, Options> = ( 314 + request: Req, 315 + options: Options, 316 + ) => Req | Promise<Req>; 317 + 318 + type ResInterceptor<Res, Req, Options> = ( 319 + response: Res, 320 + request: Req, 321 + options: Options, 322 + ) => Res | Promise<Res>; 323 + 324 + class Interceptors<Interceptor> { 325 + _fns: (Interceptor | null)[]; 326 + 327 + constructor() { 328 + this._fns = []; 329 + } 330 + 331 + clear() { 332 + this._fns = []; 333 + } 334 + 335 + getInterceptorIndex(id: number | Interceptor): number { 336 + if (typeof id === 'number') { 337 + return this._fns[id] ? id : -1; 338 + } else { 339 + return this._fns.indexOf(id); 340 + } 341 + } 342 + exists(id: number | Interceptor) { 343 + const index = this.getInterceptorIndex(id); 344 + return !!this._fns[index]; 345 + } 346 + 347 + eject(id: number | Interceptor) { 348 + const index = this.getInterceptorIndex(id); 349 + if (this._fns[index]) { 350 + this._fns[index] = null; 351 + } 352 + } 353 + 354 + update(id: number | Interceptor, fn: Interceptor) { 355 + const index = this.getInterceptorIndex(id); 356 + if (this._fns[index]) { 357 + this._fns[index] = fn; 358 + return id; 359 + } else { 360 + return false; 361 + } 362 + } 363 + 364 + use(fn: Interceptor) { 365 + this._fns = [...this._fns, fn]; 366 + return this._fns.length - 1; 367 + } 368 + } 369 + 370 + // `createInterceptors()` response, meant for external use as it does not 371 + // expose internals 372 + export interface Middleware<Req, Res, Err, Options> { 373 + error: Pick< 374 + Interceptors<ErrInterceptor<Err, Res, Req, Options>>, 375 + 'eject' | 'use' 376 + >; 377 + request: Pick<Interceptors<ReqInterceptor<Req, Options>>, 'eject' | 'use'>; 378 + response: Pick< 379 + Interceptors<ResInterceptor<Res, Req, Options>>, 380 + 'eject' | 'use' 381 + >; 382 + } 383 + 384 + // do not add `Middleware` as return type so we can use _fns internally 385 + export const createInterceptors = <Req, Res, Err, Options>() => ({ 386 + error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(), 387 + request: new Interceptors<ReqInterceptor<Req, Options>>(), 388 + response: new Interceptors<ResInterceptor<Res, Req, Options>>(), 389 + }); 390 + 391 + const defaultQuerySerializer = createQuerySerializer({ 392 + allowReserved: false, 393 + array: { 394 + explode: true, 395 + style: 'form', 396 + }, 397 + object: { 398 + explode: true, 399 + style: 'deepObject', 400 + }, 401 + }); 402 + 403 + const defaultHeaders = { 404 + 'Content-Type': 'application/json', 405 + }; 406 + 407 + export const createConfig = <T extends ClientOptions = ClientOptions>( 408 + override: Config<Omit<ClientOptions, keyof T> & T> = {}, 409 + ): Config<Omit<ClientOptions, keyof T> & T> => ({ 410 + ...jsonBodySerializer, 411 + headers: defaultHeaders, 412 + parseAs: 'auto', 413 + querySerializer: defaultQuerySerializer, 414 + ...override, 415 + });
+40
examples/openapi-ts-fastify/src/client/core/auth.ts
··· 1 + export type AuthToken = string | undefined; 2 + 3 + export interface Auth { 4 + /** 5 + * Which part of the request do we use to send the auth? 6 + * 7 + * @default 'header' 8 + */ 9 + in?: 'header' | 'query' | 'cookie'; 10 + /** 11 + * Header or query parameter name. 12 + * 13 + * @default 'Authorization' 14 + */ 15 + name?: string; 16 + scheme?: 'basic' | 'bearer'; 17 + type: 'apiKey' | 'http'; 18 + } 19 + 20 + export const getAuthToken = async ( 21 + auth: Auth, 22 + callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken, 23 + ): Promise<string | undefined> => { 24 + const token = 25 + typeof callback === 'function' ? await callback(auth) : callback; 26 + 27 + if (!token) { 28 + return; 29 + } 30 + 31 + if (auth.scheme === 'bearer') { 32 + return `Bearer ${token}`; 33 + } 34 + 35 + if (auth.scheme === 'basic') { 36 + return `Basic ${btoa(token)}`; 37 + } 38 + 39 + return token; 40 + };
+84
examples/openapi-ts-fastify/src/client/core/bodySerializer.ts
··· 1 + import type { 2 + ArrayStyle, 3 + ObjectStyle, 4 + SerializerOptions, 5 + } from './pathSerializer'; 6 + 7 + export type QuerySerializer = (query: Record<string, unknown>) => string; 8 + 9 + export type BodySerializer = (body: any) => any; 10 + 11 + export interface QuerySerializerOptions { 12 + allowReserved?: boolean; 13 + array?: SerializerOptions<ArrayStyle>; 14 + object?: SerializerOptions<ObjectStyle>; 15 + } 16 + 17 + const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { 18 + if (typeof value === 'string' || value instanceof Blob) { 19 + data.append(key, value); 20 + } else { 21 + data.append(key, JSON.stringify(value)); 22 + } 23 + }; 24 + 25 + const serializeUrlSearchParamsPair = ( 26 + data: URLSearchParams, 27 + key: string, 28 + value: unknown, 29 + ) => { 30 + if (typeof value === 'string') { 31 + data.append(key, value); 32 + } else { 33 + data.append(key, JSON.stringify(value)); 34 + } 35 + }; 36 + 37 + export const formDataBodySerializer = { 38 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 39 + body: T, 40 + ) => { 41 + const data = new FormData(); 42 + 43 + Object.entries(body).forEach(([key, value]) => { 44 + if (value === undefined || value === null) { 45 + return; 46 + } 47 + if (Array.isArray(value)) { 48 + value.forEach((v) => serializeFormDataPair(data, key, v)); 49 + } else { 50 + serializeFormDataPair(data, key, value); 51 + } 52 + }); 53 + 54 + return data; 55 + }, 56 + }; 57 + 58 + export const jsonBodySerializer = { 59 + bodySerializer: <T>(body: T) => 60 + JSON.stringify(body, (key, value) => 61 + typeof value === 'bigint' ? value.toString() : value, 62 + ), 63 + }; 64 + 65 + export const urlSearchParamsBodySerializer = { 66 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 67 + body: T, 68 + ) => { 69 + const data = new URLSearchParams(); 70 + 71 + Object.entries(body).forEach(([key, value]) => { 72 + if (value === undefined || value === null) { 73 + return; 74 + } 75 + if (Array.isArray(value)) { 76 + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); 77 + } else { 78 + serializeUrlSearchParamsPair(data, key, value); 79 + } 80 + }); 81 + 82 + return data.toString(); 83 + }, 84 + };
+141
examples/openapi-ts-fastify/src/client/core/params.ts
··· 1 + type Slot = 'body' | 'headers' | 'path' | 'query'; 2 + 3 + export type Field = 4 + | { 5 + in: Exclude<Slot, 'body'>; 6 + key: string; 7 + map?: string; 8 + } 9 + | { 10 + in: Extract<Slot, 'body'>; 11 + key?: string; 12 + map?: string; 13 + }; 14 + 15 + export interface Fields { 16 + allowExtra?: Partial<Record<Slot, boolean>>; 17 + args?: ReadonlyArray<Field>; 18 + } 19 + 20 + export type FieldsConfig = ReadonlyArray<Field | Fields>; 21 + 22 + const extraPrefixesMap: Record<string, Slot> = { 23 + $body_: 'body', 24 + $headers_: 'headers', 25 + $path_: 'path', 26 + $query_: 'query', 27 + }; 28 + const extraPrefixes = Object.entries(extraPrefixesMap); 29 + 30 + type KeyMap = Map< 31 + string, 32 + { 33 + in: Slot; 34 + map?: string; 35 + } 36 + >; 37 + 38 + const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { 39 + if (!map) { 40 + map = new Map(); 41 + } 42 + 43 + for (const config of fields) { 44 + if ('in' in config) { 45 + if (config.key) { 46 + map.set(config.key, { 47 + in: config.in, 48 + map: config.map, 49 + }); 50 + } 51 + } else if (config.args) { 52 + buildKeyMap(config.args, map); 53 + } 54 + } 55 + 56 + return map; 57 + }; 58 + 59 + interface Params { 60 + body: unknown; 61 + headers: Record<string, unknown>; 62 + path: Record<string, unknown>; 63 + query: Record<string, unknown>; 64 + } 65 + 66 + const stripEmptySlots = (params: Params) => { 67 + for (const [slot, value] of Object.entries(params)) { 68 + if (value && typeof value === 'object' && !Object.keys(value).length) { 69 + delete params[slot as Slot]; 70 + } 71 + } 72 + }; 73 + 74 + export const buildClientParams = ( 75 + args: ReadonlyArray<unknown>, 76 + fields: FieldsConfig, 77 + ) => { 78 + const params: Params = { 79 + body: {}, 80 + headers: {}, 81 + path: {}, 82 + query: {}, 83 + }; 84 + 85 + const map = buildKeyMap(fields); 86 + 87 + let config: FieldsConfig[number] | undefined; 88 + 89 + for (const [index, arg] of args.entries()) { 90 + if (fields[index]) { 91 + config = fields[index]; 92 + } 93 + 94 + if (!config) { 95 + continue; 96 + } 97 + 98 + if ('in' in config) { 99 + if (config.key) { 100 + const field = map.get(config.key)!; 101 + const name = field.map || config.key; 102 + (params[field.in] as Record<string, unknown>)[name] = arg; 103 + } else { 104 + params.body = arg; 105 + } 106 + } else { 107 + for (const [key, value] of Object.entries(arg ?? {})) { 108 + const field = map.get(key); 109 + 110 + if (field) { 111 + const name = field.map || key; 112 + (params[field.in] as Record<string, unknown>)[name] = value; 113 + } else { 114 + const extra = extraPrefixes.find(([prefix]) => 115 + key.startsWith(prefix), 116 + ); 117 + 118 + if (extra) { 119 + const [prefix, slot] = extra; 120 + (params[slot] as Record<string, unknown>)[ 121 + key.slice(prefix.length) 122 + ] = value; 123 + } else { 124 + for (const [slot, allowed] of Object.entries( 125 + config.allowExtra ?? {}, 126 + )) { 127 + if (allowed) { 128 + (params[slot as Slot] as Record<string, unknown>)[key] = value; 129 + break; 130 + } 131 + } 132 + } 133 + } 134 + } 135 + } 136 + } 137 + 138 + stripEmptySlots(params); 139 + 140 + return params; 141 + };
+179
examples/openapi-ts-fastify/src/client/core/pathSerializer.ts
··· 1 + interface SerializeOptions<T> 2 + extends SerializePrimitiveOptions, 3 + SerializerOptions<T> {} 4 + 5 + interface SerializePrimitiveOptions { 6 + allowReserved?: boolean; 7 + name: string; 8 + } 9 + 10 + export interface SerializerOptions<T> { 11 + /** 12 + * @default true 13 + */ 14 + explode: boolean; 15 + style: T; 16 + } 17 + 18 + export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; 19 + export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 20 + type MatrixStyle = 'label' | 'matrix' | 'simple'; 21 + export type ObjectStyle = 'form' | 'deepObject'; 22 + type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; 23 + 24 + interface SerializePrimitiveParam extends SerializePrimitiveOptions { 25 + value: string; 26 + } 27 + 28 + export const separatorArrayExplode = (style: ArraySeparatorStyle) => { 29 + switch (style) { 30 + case 'label': 31 + return '.'; 32 + case 'matrix': 33 + return ';'; 34 + case 'simple': 35 + return ','; 36 + default: 37 + return '&'; 38 + } 39 + }; 40 + 41 + export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { 42 + switch (style) { 43 + case 'form': 44 + return ','; 45 + case 'pipeDelimited': 46 + return '|'; 47 + case 'spaceDelimited': 48 + return '%20'; 49 + default: 50 + return ','; 51 + } 52 + }; 53 + 54 + export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { 55 + switch (style) { 56 + case 'label': 57 + return '.'; 58 + case 'matrix': 59 + return ';'; 60 + case 'simple': 61 + return ','; 62 + default: 63 + return '&'; 64 + } 65 + }; 66 + 67 + export const serializeArrayParam = ({ 68 + allowReserved, 69 + explode, 70 + name, 71 + style, 72 + value, 73 + }: SerializeOptions<ArraySeparatorStyle> & { 74 + value: unknown[]; 75 + }) => { 76 + if (!explode) { 77 + const joinedValues = ( 78 + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) 79 + ).join(separatorArrayNoExplode(style)); 80 + switch (style) { 81 + case 'label': 82 + return `.${joinedValues}`; 83 + case 'matrix': 84 + return `;${name}=${joinedValues}`; 85 + case 'simple': 86 + return joinedValues; 87 + default: 88 + return `${name}=${joinedValues}`; 89 + } 90 + } 91 + 92 + const separator = separatorArrayExplode(style); 93 + const joinedValues = value 94 + .map((v) => { 95 + if (style === 'label' || style === 'simple') { 96 + return allowReserved ? v : encodeURIComponent(v as string); 97 + } 98 + 99 + return serializePrimitiveParam({ 100 + allowReserved, 101 + name, 102 + value: v as string, 103 + }); 104 + }) 105 + .join(separator); 106 + return style === 'label' || style === 'matrix' 107 + ? separator + joinedValues 108 + : joinedValues; 109 + }; 110 + 111 + export const serializePrimitiveParam = ({ 112 + allowReserved, 113 + name, 114 + value, 115 + }: SerializePrimitiveParam) => { 116 + if (value === undefined || value === null) { 117 + return ''; 118 + } 119 + 120 + if (typeof value === 'object') { 121 + throw new Error( 122 + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', 123 + ); 124 + } 125 + 126 + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; 127 + }; 128 + 129 + export const serializeObjectParam = ({ 130 + allowReserved, 131 + explode, 132 + name, 133 + style, 134 + value, 135 + valueOnly, 136 + }: SerializeOptions<ObjectSeparatorStyle> & { 137 + value: Record<string, unknown> | Date; 138 + valueOnly?: boolean; 139 + }) => { 140 + if (value instanceof Date) { 141 + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; 142 + } 143 + 144 + if (style !== 'deepObject' && !explode) { 145 + let values: string[] = []; 146 + Object.entries(value).forEach(([key, v]) => { 147 + values = [ 148 + ...values, 149 + key, 150 + allowReserved ? (v as string) : encodeURIComponent(v as string), 151 + ]; 152 + }); 153 + const joinedValues = values.join(','); 154 + switch (style) { 155 + case 'form': 156 + return `${name}=${joinedValues}`; 157 + case 'label': 158 + return `.${joinedValues}`; 159 + case 'matrix': 160 + return `;${name}=${joinedValues}`; 161 + default: 162 + return joinedValues; 163 + } 164 + } 165 + 166 + const separator = separatorObjectExplode(style); 167 + const joinedValues = Object.entries(value) 168 + .map(([key, v]) => 169 + serializePrimitiveParam({ 170 + allowReserved, 171 + name: style === 'deepObject' ? `${name}[${key}]` : key, 172 + value: v as string, 173 + }), 174 + ) 175 + .join(separator); 176 + return style === 'label' || style === 'matrix' 177 + ? separator + joinedValues 178 + : joinedValues; 179 + };
+98
examples/openapi-ts-fastify/src/client/core/types.ts
··· 1 + import type { Auth, AuthToken } from './auth'; 2 + import type { 3 + BodySerializer, 4 + QuerySerializer, 5 + QuerySerializerOptions, 6 + } from './bodySerializer'; 7 + 8 + export interface Client< 9 + RequestFn = never, 10 + Config = unknown, 11 + MethodFn = never, 12 + BuildUrlFn = never, 13 + > { 14 + /** 15 + * Returns the final request URL. 16 + */ 17 + buildUrl: BuildUrlFn; 18 + connect: MethodFn; 19 + delete: MethodFn; 20 + get: MethodFn; 21 + getConfig: () => Config; 22 + head: MethodFn; 23 + options: MethodFn; 24 + patch: MethodFn; 25 + post: MethodFn; 26 + put: MethodFn; 27 + request: RequestFn; 28 + setConfig: (config: Config) => Config; 29 + trace: MethodFn; 30 + } 31 + 32 + export interface Config { 33 + /** 34 + * Auth token or a function returning auth token. The resolved value will be 35 + * added to the request payload as defined by its `security` array. 36 + */ 37 + auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken; 38 + /** 39 + * A function for serializing request body parameter. By default, 40 + * {@link JSON.stringify()} will be used. 41 + */ 42 + bodySerializer?: BodySerializer | null; 43 + /** 44 + * An object containing any HTTP headers that you want to pre-populate your 45 + * `Headers` object with. 46 + * 47 + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} 48 + */ 49 + headers?: 50 + | RequestInit['headers'] 51 + | Record< 52 + string, 53 + | string 54 + | number 55 + | boolean 56 + | (string | number | boolean)[] 57 + | null 58 + | undefined 59 + | unknown 60 + >; 61 + /** 62 + * The request method. 63 + * 64 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} 65 + */ 66 + method?: 67 + | 'CONNECT' 68 + | 'DELETE' 69 + | 'GET' 70 + | 'HEAD' 71 + | 'OPTIONS' 72 + | 'PATCH' 73 + | 'POST' 74 + | 'PUT' 75 + | 'TRACE'; 76 + /** 77 + * A function for serializing request query parameters. By default, arrays 78 + * will be exploded in form style, objects will be exploded in deepObject 79 + * style, and reserved characters are percent-encoded. 80 + * 81 + * This method will have no effect if the native `paramsSerializer()` Axios 82 + * API function is used. 83 + * 84 + * {@link https://swagger.io/docs/specification/serialization/#query View examples} 85 + */ 86 + querySerializer?: QuerySerializer | QuerySerializerOptions; 87 + /** 88 + * A function transforming response data before it's returned. This is useful 89 + * for post-processing data, e.g. converting ISO strings into Date objects. 90 + */ 91 + responseTransformer?: (data: unknown) => Promise<unknown>; 92 + /** 93 + * A function validating response data. This is useful if you want to ensure 94 + * the response conforms to the desired shape, so it can be safely passed to 95 + * the transformers and returned to the user. 96 + */ 97 + responseValidator?: (data: unknown) => Promise<unknown>; 98 + }
+1 -6
examples/openapi-ts-fastify/src/client/sdk.gen.ts
··· 1 1 // This file is auto-generated by @hey-api/openapi-ts 2 2 3 - import type { 4 - Client, 5 - Options as ClientOptions, 6 - TDataShape, 7 - } from '@hey-api/client-fetch'; 8 - 3 + import type { Client, Options as ClientOptions, TDataShape } from './client'; 9 4 import { client as _heyApiClient } from './client.gen'; 10 5 import type { 11 6 CreatePetsData,
-1
examples/openapi-ts-fetch/package.json
··· 12 12 "typecheck": "tsc --noEmit" 13 13 }, 14 14 "dependencies": { 15 - "@hey-api/client-fetch": "workspace:*", 16 15 "@radix-ui/react-form": "0.1.1", 17 16 "@radix-ui/react-icons": "1.3.2", 18 17 "@radix-ui/themes": "3.1.6",
+1 -1
examples/openapi-ts-fetch/src/App.tsx
··· 1 1 import './App.css'; 2 2 3 - import { createClient } from '@hey-api/client-fetch'; 4 3 import * as Form from '@radix-ui/react-form'; 5 4 import { DownloadIcon, PlusIcon, ReloadIcon } from '@radix-ui/react-icons'; 6 5 import { ··· 17 16 } from '@radix-ui/themes'; 18 17 import { useState } from 'react'; 19 18 19 + import { createClient } from './client/client'; 20 20 import { PetSchema } from './client/schemas.gen'; 21 21 import { addPet, getPetById, updatePet } from './client/sdk.gen'; 22 22 import type { Pet } from './client/types.gen';
+1 -2
examples/openapi-ts-fetch/src/client/client.gen.ts
··· 5 5 type Config, 6 6 createClient, 7 7 createConfig, 8 - } from '@hey-api/client-fetch'; 9 - 8 + } from './client'; 10 9 import type { ClientOptions } from './types.gen'; 11 10 12 11 /**
+181
examples/openapi-ts-fetch/src/client/client/client.ts
··· 1 + import type { Client, Config, RequestOptions } from './types'; 2 + import { 3 + buildUrl, 4 + createConfig, 5 + createInterceptors, 6 + getParseAs, 7 + mergeConfigs, 8 + mergeHeaders, 9 + setAuthParams, 10 + } from './utils'; 11 + 12 + type ReqInit = Omit<RequestInit, 'body' | 'headers'> & { 13 + body?: any; 14 + headers: ReturnType<typeof mergeHeaders>; 15 + }; 16 + 17 + export const createClient = (config: Config = {}): Client => { 18 + let _config = mergeConfigs(createConfig(), config); 19 + 20 + const getConfig = (): Config => ({ ..._config }); 21 + 22 + const setConfig = (config: Config): Config => { 23 + _config = mergeConfigs(_config, config); 24 + return getConfig(); 25 + }; 26 + 27 + const interceptors = createInterceptors< 28 + Request, 29 + Response, 30 + unknown, 31 + RequestOptions 32 + >(); 33 + 34 + const request: Client['request'] = async (options) => { 35 + const opts = { 36 + ..._config, 37 + ...options, 38 + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, 39 + headers: mergeHeaders(_config.headers, options.headers), 40 + }; 41 + 42 + if (opts.security) { 43 + await setAuthParams({ 44 + ...opts, 45 + security: opts.security, 46 + }); 47 + } 48 + 49 + if (opts.body && opts.bodySerializer) { 50 + opts.body = opts.bodySerializer(opts.body); 51 + } 52 + 53 + // remove Content-Type header if body is empty to avoid sending invalid requests 54 + if (opts.body === undefined || opts.body === '') { 55 + opts.headers.delete('Content-Type'); 56 + } 57 + 58 + const url = buildUrl(opts); 59 + const requestInit: ReqInit = { 60 + redirect: 'follow', 61 + ...opts, 62 + }; 63 + 64 + let request = new Request(url, requestInit); 65 + 66 + for (const fn of interceptors.request._fns) { 67 + if (fn) { 68 + request = await fn(request, opts); 69 + } 70 + } 71 + 72 + // fetch must be assigned here, otherwise it would throw the error: 73 + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation 74 + const _fetch = opts.fetch!; 75 + let response = await _fetch(request); 76 + 77 + for (const fn of interceptors.response._fns) { 78 + if (fn) { 79 + response = await fn(response, request, opts); 80 + } 81 + } 82 + 83 + const result = { 84 + request, 85 + response, 86 + }; 87 + 88 + if (response.ok) { 89 + if ( 90 + response.status === 204 || 91 + response.headers.get('Content-Length') === '0' 92 + ) { 93 + return opts.responseStyle === 'data' 94 + ? {} 95 + : { 96 + data: {}, 97 + ...result, 98 + }; 99 + } 100 + 101 + const parseAs = 102 + (opts.parseAs === 'auto' 103 + ? getParseAs(response.headers.get('Content-Type')) 104 + : opts.parseAs) ?? 'json'; 105 + 106 + if (parseAs === 'stream') { 107 + return opts.responseStyle === 'data' 108 + ? response.body 109 + : { 110 + data: response.body, 111 + ...result, 112 + }; 113 + } 114 + 115 + let data = await response[parseAs](); 116 + if (parseAs === 'json') { 117 + if (opts.responseValidator) { 118 + await opts.responseValidator(data); 119 + } 120 + 121 + if (opts.responseTransformer) { 122 + data = await opts.responseTransformer(data); 123 + } 124 + } 125 + 126 + return opts.responseStyle === 'data' 127 + ? data 128 + : { 129 + data, 130 + ...result, 131 + }; 132 + } 133 + 134 + let error = await response.text(); 135 + 136 + try { 137 + error = JSON.parse(error); 138 + } catch { 139 + // noop 140 + } 141 + 142 + let finalError = error; 143 + 144 + for (const fn of interceptors.error._fns) { 145 + if (fn) { 146 + finalError = (await fn(error, response, request, opts)) as string; 147 + } 148 + } 149 + 150 + finalError = finalError || ({} as string); 151 + 152 + if (opts.throwOnError) { 153 + throw finalError; 154 + } 155 + 156 + // TODO: we probably want to return error and improve types 157 + return opts.responseStyle === 'data' 158 + ? undefined 159 + : { 160 + error: finalError, 161 + ...result, 162 + }; 163 + }; 164 + 165 + return { 166 + buildUrl, 167 + connect: (options) => request({ ...options, method: 'CONNECT' }), 168 + delete: (options) => request({ ...options, method: 'DELETE' }), 169 + get: (options) => request({ ...options, method: 'GET' }), 170 + getConfig, 171 + head: (options) => request({ ...options, method: 'HEAD' }), 172 + interceptors, 173 + options: (options) => request({ ...options, method: 'OPTIONS' }), 174 + patch: (options) => request({ ...options, method: 'PATCH' }), 175 + post: (options) => request({ ...options, method: 'POST' }), 176 + put: (options) => request({ ...options, method: 'PUT' }), 177 + request, 178 + setConfig, 179 + trace: (options) => request({ ...options, method: 'TRACE' }), 180 + }; 181 + };
+22
examples/openapi-ts-fetch/src/client/client/index.ts
··· 1 + export type { Auth } from '../core/auth'; 2 + export type { QuerySerializerOptions } from '../core/bodySerializer'; 3 + export { 4 + formDataBodySerializer, 5 + jsonBodySerializer, 6 + urlSearchParamsBodySerializer, 7 + } from '../core/bodySerializer'; 8 + export { buildClientParams } from '../core/params'; 9 + export { createClient } from './client'; 10 + export type { 11 + Client, 12 + ClientOptions, 13 + Config, 14 + CreateClientConfig, 15 + Options, 16 + OptionsLegacyParser, 17 + RequestOptions, 18 + RequestResult, 19 + ResponseStyle, 20 + TDataShape, 21 + } from './types'; 22 + export { createConfig, mergeHeaders } from './utils';
+212
examples/openapi-ts-fetch/src/client/client/types.ts
··· 1 + import type { Auth } from '../core/auth'; 2 + import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; 3 + import type { Middleware } from './utils'; 4 + 5 + export type ResponseStyle = 'data' | 'fields'; 6 + 7 + export interface Config<T extends ClientOptions = ClientOptions> 8 + extends Omit<RequestInit, 'body' | 'headers' | 'method'>, 9 + CoreConfig { 10 + /** 11 + * Base URL for all requests made by this client. 12 + */ 13 + baseUrl?: T['baseUrl']; 14 + /** 15 + * Fetch API implementation. You can use this option to provide a custom 16 + * fetch instance. 17 + * 18 + * @default globalThis.fetch 19 + */ 20 + fetch?: (request: Request) => ReturnType<typeof fetch>; 21 + /** 22 + * Please don't use the Fetch client for Next.js applications. The `next` 23 + * options won't have any effect. 24 + * 25 + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. 26 + */ 27 + next?: never; 28 + /** 29 + * Return the response data parsed in a specified format. By default, `auto` 30 + * will infer the appropriate method from the `Content-Type` response header. 31 + * You can override this behavior with any of the {@link Body} methods. 32 + * Select `stream` if you don't want to parse response data at all. 33 + * 34 + * @default 'auto' 35 + */ 36 + parseAs?: Exclude<keyof Body, 'body' | 'bodyUsed'> | 'auto' | 'stream'; 37 + /** 38 + * Should we return only data or multiple fields (data, error, response, etc.)? 39 + * 40 + * @default 'fields' 41 + */ 42 + responseStyle?: ResponseStyle; 43 + /** 44 + * Throw an error instead of returning it in the response? 45 + * 46 + * @default false 47 + */ 48 + throwOnError?: T['throwOnError']; 49 + } 50 + 51 + export interface RequestOptions< 52 + TResponseStyle extends ResponseStyle = 'fields', 53 + ThrowOnError extends boolean = boolean, 54 + Url extends string = string, 55 + > extends Config<{ 56 + responseStyle: TResponseStyle; 57 + throwOnError: ThrowOnError; 58 + }> { 59 + /** 60 + * Any body that you want to add to your request. 61 + * 62 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} 63 + */ 64 + body?: unknown; 65 + path?: Record<string, unknown>; 66 + query?: Record<string, unknown>; 67 + /** 68 + * Security mechanism(s) to use for the request. 69 + */ 70 + security?: ReadonlyArray<Auth>; 71 + url: Url; 72 + } 73 + 74 + export type RequestResult< 75 + TData = unknown, 76 + TError = unknown, 77 + ThrowOnError extends boolean = boolean, 78 + TResponseStyle extends ResponseStyle = 'fields', 79 + > = ThrowOnError extends true 80 + ? Promise< 81 + TResponseStyle extends 'data' 82 + ? TData extends Record<string, unknown> 83 + ? TData[keyof TData] 84 + : TData 85 + : { 86 + data: TData extends Record<string, unknown> 87 + ? TData[keyof TData] 88 + : TData; 89 + request: Request; 90 + response: Response; 91 + } 92 + > 93 + : Promise< 94 + TResponseStyle extends 'data' 95 + ? 96 + | (TData extends Record<string, unknown> 97 + ? TData[keyof TData] 98 + : TData) 99 + | undefined 100 + : ( 101 + | { 102 + data: TData extends Record<string, unknown> 103 + ? TData[keyof TData] 104 + : TData; 105 + error: undefined; 106 + } 107 + | { 108 + data: undefined; 109 + error: TError extends Record<string, unknown> 110 + ? TError[keyof TError] 111 + : TError; 112 + } 113 + ) & { 114 + request: Request; 115 + response: Response; 116 + } 117 + >; 118 + 119 + export interface ClientOptions { 120 + baseUrl?: string; 121 + responseStyle?: ResponseStyle; 122 + throwOnError?: boolean; 123 + } 124 + 125 + type MethodFn = < 126 + TData = unknown, 127 + TError = unknown, 128 + ThrowOnError extends boolean = false, 129 + TResponseStyle extends ResponseStyle = 'fields', 130 + >( 131 + options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'>, 132 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 133 + 134 + type RequestFn = < 135 + TData = unknown, 136 + TError = unknown, 137 + ThrowOnError extends boolean = false, 138 + TResponseStyle extends ResponseStyle = 'fields', 139 + >( 140 + options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'> & 141 + Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, 'method'>, 142 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 143 + 144 + type BuildUrlFn = < 145 + TData extends { 146 + body?: unknown; 147 + path?: Record<string, unknown>; 148 + query?: Record<string, unknown>; 149 + url: string; 150 + }, 151 + >( 152 + options: Pick<TData, 'url'> & Options<TData>, 153 + ) => string; 154 + 155 + export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & { 156 + interceptors: Middleware<Request, Response, unknown, RequestOptions>; 157 + }; 158 + 159 + /** 160 + * The `createClientConfig()` function will be called on client initialization 161 + * and the returned object will become the client's initial configuration. 162 + * 163 + * You may want to initialize your client this way instead of calling 164 + * `setConfig()`. This is useful for example if you're using Next.js 165 + * to ensure your client always has the correct values. 166 + */ 167 + export type CreateClientConfig<T extends ClientOptions = ClientOptions> = ( 168 + override?: Config<ClientOptions & T>, 169 + ) => Config<Required<ClientOptions> & T>; 170 + 171 + export interface TDataShape { 172 + body?: unknown; 173 + headers?: unknown; 174 + path?: unknown; 175 + query?: unknown; 176 + url: string; 177 + } 178 + 179 + type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>; 180 + 181 + export type Options< 182 + TData extends TDataShape = TDataShape, 183 + ThrowOnError extends boolean = boolean, 184 + TResponseStyle extends ResponseStyle = 'fields', 185 + > = OmitKeys< 186 + RequestOptions<TResponseStyle, ThrowOnError>, 187 + 'body' | 'path' | 'query' | 'url' 188 + > & 189 + Omit<TData, 'url'>; 190 + 191 + export type OptionsLegacyParser< 192 + TData = unknown, 193 + ThrowOnError extends boolean = boolean, 194 + TResponseStyle extends ResponseStyle = 'fields', 195 + > = TData extends { body?: any } 196 + ? TData extends { headers?: any } 197 + ? OmitKeys< 198 + RequestOptions<TResponseStyle, ThrowOnError>, 199 + 'body' | 'headers' | 'url' 200 + > & 201 + TData 202 + : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'body' | 'url'> & 203 + TData & 204 + Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'headers'> 205 + : TData extends { headers?: any } 206 + ? OmitKeys< 207 + RequestOptions<TResponseStyle, ThrowOnError>, 208 + 'headers' | 'url' 209 + > & 210 + TData & 211 + Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'body'> 212 + : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'url'> & TData;
+415
examples/openapi-ts-fetch/src/client/client/utils.ts
··· 1 + import { getAuthToken } from '../core/auth'; 2 + import type { 3 + QuerySerializer, 4 + QuerySerializerOptions, 5 + } from '../core/bodySerializer'; 6 + import { jsonBodySerializer } from '../core/bodySerializer'; 7 + import { 8 + serializeArrayParam, 9 + serializeObjectParam, 10 + serializePrimitiveParam, 11 + } from '../core/pathSerializer'; 12 + import type { Client, ClientOptions, Config, RequestOptions } from './types'; 13 + 14 + interface PathSerializer { 15 + path: Record<string, unknown>; 16 + url: string; 17 + } 18 + 19 + const PATH_PARAM_RE = /\{[^{}]+\}/g; 20 + 21 + type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; 22 + type MatrixStyle = 'label' | 'matrix' | 'simple'; 23 + type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 24 + 25 + const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { 26 + let url = _url; 27 + const matches = _url.match(PATH_PARAM_RE); 28 + if (matches) { 29 + for (const match of matches) { 30 + let explode = false; 31 + let name = match.substring(1, match.length - 1); 32 + let style: ArraySeparatorStyle = 'simple'; 33 + 34 + if (name.endsWith('*')) { 35 + explode = true; 36 + name = name.substring(0, name.length - 1); 37 + } 38 + 39 + if (name.startsWith('.')) { 40 + name = name.substring(1); 41 + style = 'label'; 42 + } else if (name.startsWith(';')) { 43 + name = name.substring(1); 44 + style = 'matrix'; 45 + } 46 + 47 + const value = path[name]; 48 + 49 + if (value === undefined || value === null) { 50 + continue; 51 + } 52 + 53 + if (Array.isArray(value)) { 54 + url = url.replace( 55 + match, 56 + serializeArrayParam({ explode, name, style, value }), 57 + ); 58 + continue; 59 + } 60 + 61 + if (typeof value === 'object') { 62 + url = url.replace( 63 + match, 64 + serializeObjectParam({ 65 + explode, 66 + name, 67 + style, 68 + value: value as Record<string, unknown>, 69 + valueOnly: true, 70 + }), 71 + ); 72 + continue; 73 + } 74 + 75 + if (style === 'matrix') { 76 + url = url.replace( 77 + match, 78 + `;${serializePrimitiveParam({ 79 + name, 80 + value: value as string, 81 + })}`, 82 + ); 83 + continue; 84 + } 85 + 86 + const replaceValue = encodeURIComponent( 87 + style === 'label' ? `.${value as string}` : (value as string), 88 + ); 89 + url = url.replace(match, replaceValue); 90 + } 91 + } 92 + return url; 93 + }; 94 + 95 + export const createQuerySerializer = <T = unknown>({ 96 + allowReserved, 97 + array, 98 + object, 99 + }: QuerySerializerOptions = {}) => { 100 + const querySerializer = (queryParams: T) => { 101 + const search: string[] = []; 102 + if (queryParams && typeof queryParams === 'object') { 103 + for (const name in queryParams) { 104 + const value = queryParams[name]; 105 + 106 + if (value === undefined || value === null) { 107 + continue; 108 + } 109 + 110 + if (Array.isArray(value)) { 111 + const serializedArray = serializeArrayParam({ 112 + allowReserved, 113 + explode: true, 114 + name, 115 + style: 'form', 116 + value, 117 + ...array, 118 + }); 119 + if (serializedArray) search.push(serializedArray); 120 + } else if (typeof value === 'object') { 121 + const serializedObject = serializeObjectParam({ 122 + allowReserved, 123 + explode: true, 124 + name, 125 + style: 'deepObject', 126 + value: value as Record<string, unknown>, 127 + ...object, 128 + }); 129 + if (serializedObject) search.push(serializedObject); 130 + } else { 131 + const serializedPrimitive = serializePrimitiveParam({ 132 + allowReserved, 133 + name, 134 + value: value as string, 135 + }); 136 + if (serializedPrimitive) search.push(serializedPrimitive); 137 + } 138 + } 139 + } 140 + return search.join('&'); 141 + }; 142 + return querySerializer; 143 + }; 144 + 145 + /** 146 + * Infers parseAs value from provided Content-Type header. 147 + */ 148 + export const getParseAs = ( 149 + contentType: string | null, 150 + ): Exclude<Config['parseAs'], 'auto'> => { 151 + if (!contentType) { 152 + // If no Content-Type header is provided, the best we can do is return the raw response body, 153 + // which is effectively the same as the 'stream' option. 154 + return 'stream'; 155 + } 156 + 157 + const cleanContent = contentType.split(';')[0]?.trim(); 158 + 159 + if (!cleanContent) { 160 + return; 161 + } 162 + 163 + if ( 164 + cleanContent.startsWith('application/json') || 165 + cleanContent.endsWith('+json') 166 + ) { 167 + return 'json'; 168 + } 169 + 170 + if (cleanContent === 'multipart/form-data') { 171 + return 'formData'; 172 + } 173 + 174 + if ( 175 + ['application/', 'audio/', 'image/', 'video/'].some((type) => 176 + cleanContent.startsWith(type), 177 + ) 178 + ) { 179 + return 'blob'; 180 + } 181 + 182 + if (cleanContent.startsWith('text/')) { 183 + return 'text'; 184 + } 185 + }; 186 + 187 + export const setAuthParams = async ({ 188 + security, 189 + ...options 190 + }: Pick<Required<RequestOptions>, 'security'> & 191 + Pick<RequestOptions, 'auth' | 'query'> & { 192 + headers: Headers; 193 + }) => { 194 + for (const auth of security) { 195 + const token = await getAuthToken(auth, options.auth); 196 + 197 + if (!token) { 198 + continue; 199 + } 200 + 201 + const name = auth.name ?? 'Authorization'; 202 + 203 + switch (auth.in) { 204 + case 'query': 205 + if (!options.query) { 206 + options.query = {}; 207 + } 208 + options.query[name] = token; 209 + break; 210 + case 'cookie': 211 + options.headers.append('Cookie', `${name}=${token}`); 212 + break; 213 + case 'header': 214 + default: 215 + options.headers.set(name, token); 216 + break; 217 + } 218 + 219 + return; 220 + } 221 + }; 222 + 223 + export const buildUrl: Client['buildUrl'] = (options) => { 224 + const url = getUrl({ 225 + baseUrl: options.baseUrl as string, 226 + path: options.path, 227 + query: options.query, 228 + querySerializer: 229 + typeof options.querySerializer === 'function' 230 + ? options.querySerializer 231 + : createQuerySerializer(options.querySerializer), 232 + url: options.url, 233 + }); 234 + return url; 235 + }; 236 + 237 + export const getUrl = ({ 238 + baseUrl, 239 + path, 240 + query, 241 + querySerializer, 242 + url: _url, 243 + }: { 244 + baseUrl?: string; 245 + path?: Record<string, unknown>; 246 + query?: Record<string, unknown>; 247 + querySerializer: QuerySerializer; 248 + url: string; 249 + }) => { 250 + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; 251 + let url = (baseUrl ?? '') + pathUrl; 252 + if (path) { 253 + url = defaultPathSerializer({ path, url }); 254 + } 255 + let search = query ? querySerializer(query) : ''; 256 + if (search.startsWith('?')) { 257 + search = search.substring(1); 258 + } 259 + if (search) { 260 + url += `?${search}`; 261 + } 262 + return url; 263 + }; 264 + 265 + export const mergeConfigs = (a: Config, b: Config): Config => { 266 + const config = { ...a, ...b }; 267 + if (config.baseUrl?.endsWith('/')) { 268 + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); 269 + } 270 + config.headers = mergeHeaders(a.headers, b.headers); 271 + return config; 272 + }; 273 + 274 + export const mergeHeaders = ( 275 + ...headers: Array<Required<Config>['headers'] | undefined> 276 + ): Headers => { 277 + const mergedHeaders = new Headers(); 278 + for (const header of headers) { 279 + if (!header || typeof header !== 'object') { 280 + continue; 281 + } 282 + 283 + const iterator = 284 + header instanceof Headers ? header.entries() : Object.entries(header); 285 + 286 + for (const [key, value] of iterator) { 287 + if (value === null) { 288 + mergedHeaders.delete(key); 289 + } else if (Array.isArray(value)) { 290 + for (const v of value) { 291 + mergedHeaders.append(key, v as string); 292 + } 293 + } else if (value !== undefined) { 294 + // assume object headers are meant to be JSON stringified, i.e. their 295 + // content value in OpenAPI specification is 'application/json' 296 + mergedHeaders.set( 297 + key, 298 + typeof value === 'object' ? JSON.stringify(value) : (value as string), 299 + ); 300 + } 301 + } 302 + } 303 + return mergedHeaders; 304 + }; 305 + 306 + type ErrInterceptor<Err, Res, Req, Options> = ( 307 + error: Err, 308 + response: Res, 309 + request: Req, 310 + options: Options, 311 + ) => Err | Promise<Err>; 312 + 313 + type ReqInterceptor<Req, Options> = ( 314 + request: Req, 315 + options: Options, 316 + ) => Req | Promise<Req>; 317 + 318 + type ResInterceptor<Res, Req, Options> = ( 319 + response: Res, 320 + request: Req, 321 + options: Options, 322 + ) => Res | Promise<Res>; 323 + 324 + class Interceptors<Interceptor> { 325 + _fns: (Interceptor | null)[]; 326 + 327 + constructor() { 328 + this._fns = []; 329 + } 330 + 331 + clear() { 332 + this._fns = []; 333 + } 334 + 335 + getInterceptorIndex(id: number | Interceptor): number { 336 + if (typeof id === 'number') { 337 + return this._fns[id] ? id : -1; 338 + } else { 339 + return this._fns.indexOf(id); 340 + } 341 + } 342 + exists(id: number | Interceptor) { 343 + const index = this.getInterceptorIndex(id); 344 + return !!this._fns[index]; 345 + } 346 + 347 + eject(id: number | Interceptor) { 348 + const index = this.getInterceptorIndex(id); 349 + if (this._fns[index]) { 350 + this._fns[index] = null; 351 + } 352 + } 353 + 354 + update(id: number | Interceptor, fn: Interceptor) { 355 + const index = this.getInterceptorIndex(id); 356 + if (this._fns[index]) { 357 + this._fns[index] = fn; 358 + return id; 359 + } else { 360 + return false; 361 + } 362 + } 363 + 364 + use(fn: Interceptor) { 365 + this._fns = [...this._fns, fn]; 366 + return this._fns.length - 1; 367 + } 368 + } 369 + 370 + // `createInterceptors()` response, meant for external use as it does not 371 + // expose internals 372 + export interface Middleware<Req, Res, Err, Options> { 373 + error: Pick< 374 + Interceptors<ErrInterceptor<Err, Res, Req, Options>>, 375 + 'eject' | 'use' 376 + >; 377 + request: Pick<Interceptors<ReqInterceptor<Req, Options>>, 'eject' | 'use'>; 378 + response: Pick< 379 + Interceptors<ResInterceptor<Res, Req, Options>>, 380 + 'eject' | 'use' 381 + >; 382 + } 383 + 384 + // do not add `Middleware` as return type so we can use _fns internally 385 + export const createInterceptors = <Req, Res, Err, Options>() => ({ 386 + error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(), 387 + request: new Interceptors<ReqInterceptor<Req, Options>>(), 388 + response: new Interceptors<ResInterceptor<Res, Req, Options>>(), 389 + }); 390 + 391 + const defaultQuerySerializer = createQuerySerializer({ 392 + allowReserved: false, 393 + array: { 394 + explode: true, 395 + style: 'form', 396 + }, 397 + object: { 398 + explode: true, 399 + style: 'deepObject', 400 + }, 401 + }); 402 + 403 + const defaultHeaders = { 404 + 'Content-Type': 'application/json', 405 + }; 406 + 407 + export const createConfig = <T extends ClientOptions = ClientOptions>( 408 + override: Config<Omit<ClientOptions, keyof T> & T> = {}, 409 + ): Config<Omit<ClientOptions, keyof T> & T> => ({ 410 + ...jsonBodySerializer, 411 + headers: defaultHeaders, 412 + parseAs: 'auto', 413 + querySerializer: defaultQuerySerializer, 414 + ...override, 415 + });
+40
examples/openapi-ts-fetch/src/client/core/auth.ts
··· 1 + export type AuthToken = string | undefined; 2 + 3 + export interface Auth { 4 + /** 5 + * Which part of the request do we use to send the auth? 6 + * 7 + * @default 'header' 8 + */ 9 + in?: 'header' | 'query' | 'cookie'; 10 + /** 11 + * Header or query parameter name. 12 + * 13 + * @default 'Authorization' 14 + */ 15 + name?: string; 16 + scheme?: 'basic' | 'bearer'; 17 + type: 'apiKey' | 'http'; 18 + } 19 + 20 + export const getAuthToken = async ( 21 + auth: Auth, 22 + callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken, 23 + ): Promise<string | undefined> => { 24 + const token = 25 + typeof callback === 'function' ? await callback(auth) : callback; 26 + 27 + if (!token) { 28 + return; 29 + } 30 + 31 + if (auth.scheme === 'bearer') { 32 + return `Bearer ${token}`; 33 + } 34 + 35 + if (auth.scheme === 'basic') { 36 + return `Basic ${btoa(token)}`; 37 + } 38 + 39 + return token; 40 + };
+84
examples/openapi-ts-fetch/src/client/core/bodySerializer.ts
··· 1 + import type { 2 + ArrayStyle, 3 + ObjectStyle, 4 + SerializerOptions, 5 + } from './pathSerializer'; 6 + 7 + export type QuerySerializer = (query: Record<string, unknown>) => string; 8 + 9 + export type BodySerializer = (body: any) => any; 10 + 11 + export interface QuerySerializerOptions { 12 + allowReserved?: boolean; 13 + array?: SerializerOptions<ArrayStyle>; 14 + object?: SerializerOptions<ObjectStyle>; 15 + } 16 + 17 + const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { 18 + if (typeof value === 'string' || value instanceof Blob) { 19 + data.append(key, value); 20 + } else { 21 + data.append(key, JSON.stringify(value)); 22 + } 23 + }; 24 + 25 + const serializeUrlSearchParamsPair = ( 26 + data: URLSearchParams, 27 + key: string, 28 + value: unknown, 29 + ) => { 30 + if (typeof value === 'string') { 31 + data.append(key, value); 32 + } else { 33 + data.append(key, JSON.stringify(value)); 34 + } 35 + }; 36 + 37 + export const formDataBodySerializer = { 38 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 39 + body: T, 40 + ) => { 41 + const data = new FormData(); 42 + 43 + Object.entries(body).forEach(([key, value]) => { 44 + if (value === undefined || value === null) { 45 + return; 46 + } 47 + if (Array.isArray(value)) { 48 + value.forEach((v) => serializeFormDataPair(data, key, v)); 49 + } else { 50 + serializeFormDataPair(data, key, value); 51 + } 52 + }); 53 + 54 + return data; 55 + }, 56 + }; 57 + 58 + export const jsonBodySerializer = { 59 + bodySerializer: <T>(body: T) => 60 + JSON.stringify(body, (key, value) => 61 + typeof value === 'bigint' ? value.toString() : value, 62 + ), 63 + }; 64 + 65 + export const urlSearchParamsBodySerializer = { 66 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 67 + body: T, 68 + ) => { 69 + const data = new URLSearchParams(); 70 + 71 + Object.entries(body).forEach(([key, value]) => { 72 + if (value === undefined || value === null) { 73 + return; 74 + } 75 + if (Array.isArray(value)) { 76 + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); 77 + } else { 78 + serializeUrlSearchParamsPair(data, key, value); 79 + } 80 + }); 81 + 82 + return data.toString(); 83 + }, 84 + };
+141
examples/openapi-ts-fetch/src/client/core/params.ts
··· 1 + type Slot = 'body' | 'headers' | 'path' | 'query'; 2 + 3 + export type Field = 4 + | { 5 + in: Exclude<Slot, 'body'>; 6 + key: string; 7 + map?: string; 8 + } 9 + | { 10 + in: Extract<Slot, 'body'>; 11 + key?: string; 12 + map?: string; 13 + }; 14 + 15 + export interface Fields { 16 + allowExtra?: Partial<Record<Slot, boolean>>; 17 + args?: ReadonlyArray<Field>; 18 + } 19 + 20 + export type FieldsConfig = ReadonlyArray<Field | Fields>; 21 + 22 + const extraPrefixesMap: Record<string, Slot> = { 23 + $body_: 'body', 24 + $headers_: 'headers', 25 + $path_: 'path', 26 + $query_: 'query', 27 + }; 28 + const extraPrefixes = Object.entries(extraPrefixesMap); 29 + 30 + type KeyMap = Map< 31 + string, 32 + { 33 + in: Slot; 34 + map?: string; 35 + } 36 + >; 37 + 38 + const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { 39 + if (!map) { 40 + map = new Map(); 41 + } 42 + 43 + for (const config of fields) { 44 + if ('in' in config) { 45 + if (config.key) { 46 + map.set(config.key, { 47 + in: config.in, 48 + map: config.map, 49 + }); 50 + } 51 + } else if (config.args) { 52 + buildKeyMap(config.args, map); 53 + } 54 + } 55 + 56 + return map; 57 + }; 58 + 59 + interface Params { 60 + body: unknown; 61 + headers: Record<string, unknown>; 62 + path: Record<string, unknown>; 63 + query: Record<string, unknown>; 64 + } 65 + 66 + const stripEmptySlots = (params: Params) => { 67 + for (const [slot, value] of Object.entries(params)) { 68 + if (value && typeof value === 'object' && !Object.keys(value).length) { 69 + delete params[slot as Slot]; 70 + } 71 + } 72 + }; 73 + 74 + export const buildClientParams = ( 75 + args: ReadonlyArray<unknown>, 76 + fields: FieldsConfig, 77 + ) => { 78 + const params: Params = { 79 + body: {}, 80 + headers: {}, 81 + path: {}, 82 + query: {}, 83 + }; 84 + 85 + const map = buildKeyMap(fields); 86 + 87 + let config: FieldsConfig[number] | undefined; 88 + 89 + for (const [index, arg] of args.entries()) { 90 + if (fields[index]) { 91 + config = fields[index]; 92 + } 93 + 94 + if (!config) { 95 + continue; 96 + } 97 + 98 + if ('in' in config) { 99 + if (config.key) { 100 + const field = map.get(config.key)!; 101 + const name = field.map || config.key; 102 + (params[field.in] as Record<string, unknown>)[name] = arg; 103 + } else { 104 + params.body = arg; 105 + } 106 + } else { 107 + for (const [key, value] of Object.entries(arg ?? {})) { 108 + const field = map.get(key); 109 + 110 + if (field) { 111 + const name = field.map || key; 112 + (params[field.in] as Record<string, unknown>)[name] = value; 113 + } else { 114 + const extra = extraPrefixes.find(([prefix]) => 115 + key.startsWith(prefix), 116 + ); 117 + 118 + if (extra) { 119 + const [prefix, slot] = extra; 120 + (params[slot] as Record<string, unknown>)[ 121 + key.slice(prefix.length) 122 + ] = value; 123 + } else { 124 + for (const [slot, allowed] of Object.entries( 125 + config.allowExtra ?? {}, 126 + )) { 127 + if (allowed) { 128 + (params[slot as Slot] as Record<string, unknown>)[key] = value; 129 + break; 130 + } 131 + } 132 + } 133 + } 134 + } 135 + } 136 + } 137 + 138 + stripEmptySlots(params); 139 + 140 + return params; 141 + };
+179
examples/openapi-ts-fetch/src/client/core/pathSerializer.ts
··· 1 + interface SerializeOptions<T> 2 + extends SerializePrimitiveOptions, 3 + SerializerOptions<T> {} 4 + 5 + interface SerializePrimitiveOptions { 6 + allowReserved?: boolean; 7 + name: string; 8 + } 9 + 10 + export interface SerializerOptions<T> { 11 + /** 12 + * @default true 13 + */ 14 + explode: boolean; 15 + style: T; 16 + } 17 + 18 + export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; 19 + export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 20 + type MatrixStyle = 'label' | 'matrix' | 'simple'; 21 + export type ObjectStyle = 'form' | 'deepObject'; 22 + type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; 23 + 24 + interface SerializePrimitiveParam extends SerializePrimitiveOptions { 25 + value: string; 26 + } 27 + 28 + export const separatorArrayExplode = (style: ArraySeparatorStyle) => { 29 + switch (style) { 30 + case 'label': 31 + return '.'; 32 + case 'matrix': 33 + return ';'; 34 + case 'simple': 35 + return ','; 36 + default: 37 + return '&'; 38 + } 39 + }; 40 + 41 + export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { 42 + switch (style) { 43 + case 'form': 44 + return ','; 45 + case 'pipeDelimited': 46 + return '|'; 47 + case 'spaceDelimited': 48 + return '%20'; 49 + default: 50 + return ','; 51 + } 52 + }; 53 + 54 + export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { 55 + switch (style) { 56 + case 'label': 57 + return '.'; 58 + case 'matrix': 59 + return ';'; 60 + case 'simple': 61 + return ','; 62 + default: 63 + return '&'; 64 + } 65 + }; 66 + 67 + export const serializeArrayParam = ({ 68 + allowReserved, 69 + explode, 70 + name, 71 + style, 72 + value, 73 + }: SerializeOptions<ArraySeparatorStyle> & { 74 + value: unknown[]; 75 + }) => { 76 + if (!explode) { 77 + const joinedValues = ( 78 + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) 79 + ).join(separatorArrayNoExplode(style)); 80 + switch (style) { 81 + case 'label': 82 + return `.${joinedValues}`; 83 + case 'matrix': 84 + return `;${name}=${joinedValues}`; 85 + case 'simple': 86 + return joinedValues; 87 + default: 88 + return `${name}=${joinedValues}`; 89 + } 90 + } 91 + 92 + const separator = separatorArrayExplode(style); 93 + const joinedValues = value 94 + .map((v) => { 95 + if (style === 'label' || style === 'simple') { 96 + return allowReserved ? v : encodeURIComponent(v as string); 97 + } 98 + 99 + return serializePrimitiveParam({ 100 + allowReserved, 101 + name, 102 + value: v as string, 103 + }); 104 + }) 105 + .join(separator); 106 + return style === 'label' || style === 'matrix' 107 + ? separator + joinedValues 108 + : joinedValues; 109 + }; 110 + 111 + export const serializePrimitiveParam = ({ 112 + allowReserved, 113 + name, 114 + value, 115 + }: SerializePrimitiveParam) => { 116 + if (value === undefined || value === null) { 117 + return ''; 118 + } 119 + 120 + if (typeof value === 'object') { 121 + throw new Error( 122 + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', 123 + ); 124 + } 125 + 126 + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; 127 + }; 128 + 129 + export const serializeObjectParam = ({ 130 + allowReserved, 131 + explode, 132 + name, 133 + style, 134 + value, 135 + valueOnly, 136 + }: SerializeOptions<ObjectSeparatorStyle> & { 137 + value: Record<string, unknown> | Date; 138 + valueOnly?: boolean; 139 + }) => { 140 + if (value instanceof Date) { 141 + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; 142 + } 143 + 144 + if (style !== 'deepObject' && !explode) { 145 + let values: string[] = []; 146 + Object.entries(value).forEach(([key, v]) => { 147 + values = [ 148 + ...values, 149 + key, 150 + allowReserved ? (v as string) : encodeURIComponent(v as string), 151 + ]; 152 + }); 153 + const joinedValues = values.join(','); 154 + switch (style) { 155 + case 'form': 156 + return `${name}=${joinedValues}`; 157 + case 'label': 158 + return `.${joinedValues}`; 159 + case 'matrix': 160 + return `;${name}=${joinedValues}`; 161 + default: 162 + return joinedValues; 163 + } 164 + } 165 + 166 + const separator = separatorObjectExplode(style); 167 + const joinedValues = Object.entries(value) 168 + .map(([key, v]) => 169 + serializePrimitiveParam({ 170 + allowReserved, 171 + name: style === 'deepObject' ? `${name}[${key}]` : key, 172 + value: v as string, 173 + }), 174 + ) 175 + .join(separator); 176 + return style === 'label' || style === 'matrix' 177 + ? separator + joinedValues 178 + : joinedValues; 179 + };
+98
examples/openapi-ts-fetch/src/client/core/types.ts
··· 1 + import type { Auth, AuthToken } from './auth'; 2 + import type { 3 + BodySerializer, 4 + QuerySerializer, 5 + QuerySerializerOptions, 6 + } from './bodySerializer'; 7 + 8 + export interface Client< 9 + RequestFn = never, 10 + Config = unknown, 11 + MethodFn = never, 12 + BuildUrlFn = never, 13 + > { 14 + /** 15 + * Returns the final request URL. 16 + */ 17 + buildUrl: BuildUrlFn; 18 + connect: MethodFn; 19 + delete: MethodFn; 20 + get: MethodFn; 21 + getConfig: () => Config; 22 + head: MethodFn; 23 + options: MethodFn; 24 + patch: MethodFn; 25 + post: MethodFn; 26 + put: MethodFn; 27 + request: RequestFn; 28 + setConfig: (config: Config) => Config; 29 + trace: MethodFn; 30 + } 31 + 32 + export interface Config { 33 + /** 34 + * Auth token or a function returning auth token. The resolved value will be 35 + * added to the request payload as defined by its `security` array. 36 + */ 37 + auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken; 38 + /** 39 + * A function for serializing request body parameter. By default, 40 + * {@link JSON.stringify()} will be used. 41 + */ 42 + bodySerializer?: BodySerializer | null; 43 + /** 44 + * An object containing any HTTP headers that you want to pre-populate your 45 + * `Headers` object with. 46 + * 47 + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} 48 + */ 49 + headers?: 50 + | RequestInit['headers'] 51 + | Record< 52 + string, 53 + | string 54 + | number 55 + | boolean 56 + | (string | number | boolean)[] 57 + | null 58 + | undefined 59 + | unknown 60 + >; 61 + /** 62 + * The request method. 63 + * 64 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} 65 + */ 66 + method?: 67 + | 'CONNECT' 68 + | 'DELETE' 69 + | 'GET' 70 + | 'HEAD' 71 + | 'OPTIONS' 72 + | 'PATCH' 73 + | 'POST' 74 + | 'PUT' 75 + | 'TRACE'; 76 + /** 77 + * A function for serializing request query parameters. By default, arrays 78 + * will be exploded in form style, objects will be exploded in deepObject 79 + * style, and reserved characters are percent-encoded. 80 + * 81 + * This method will have no effect if the native `paramsSerializer()` Axios 82 + * API function is used. 83 + * 84 + * {@link https://swagger.io/docs/specification/serialization/#query View examples} 85 + */ 86 + querySerializer?: QuerySerializer | QuerySerializerOptions; 87 + /** 88 + * A function transforming response data before it's returned. This is useful 89 + * for post-processing data, e.g. converting ISO strings into Date objects. 90 + */ 91 + responseTransformer?: (data: unknown) => Promise<unknown>; 92 + /** 93 + * A function validating response data. This is useful if you want to ensure 94 + * the response conforms to the desired shape, so it can be safely passed to 95 + * the transformers and returned to the user. 96 + */ 97 + responseValidator?: (data: unknown) => Promise<unknown>; 98 + }
+5 -10
examples/openapi-ts-fetch/src/client/sdk.gen.ts
··· 1 1 // This file is auto-generated by @hey-api/openapi-ts 2 2 3 - import type { 4 - Client, 5 - Options as ClientOptions, 6 - TDataShape, 7 - } from '@hey-api/client-fetch'; 8 - 3 + import type { Client, Options as ClientOptions, TDataShape } from './client'; 9 4 import { client as _heyApiClient } from './client.gen'; 10 5 import type { 11 6 AddPetData, ··· 106 101 ...options, 107 102 headers: { 108 103 'Content-Type': 'application/json', 109 - ...options?.headers, 104 + ...options.headers, 110 105 }, 111 106 }); 112 107 ··· 132 127 ...options, 133 128 headers: { 134 129 'Content-Type': 'application/json', 135 - ...options?.headers, 130 + ...options.headers, 136 131 }, 137 132 }); 138 133 ··· 273 268 ...options, 274 269 headers: { 275 270 'Content-Type': 'application/octet-stream', 276 - ...options?.headers, 271 + ...options.headers, 277 272 }, 278 273 }); 279 274 ··· 471 466 ...options, 472 467 headers: { 473 468 'Content-Type': 'application/json', 474 - ...options?.headers, 469 + ...options.headers, 475 470 }, 476 471 });
-1
examples/openapi-ts-next/package.json
··· 10 10 "start": "next start" 11 11 }, 12 12 "dependencies": { 13 - "@hey-api/client-next": "workspace:*", 14 13 "next": "15.2.4", 15 14 "react": "19.0.0", 16 15 "react-dom": "19.0.0"
+2 -3
examples/openapi-ts-next/src/client/client.gen.ts
··· 1 1 // This file is auto-generated by @hey-api/openapi-ts 2 2 3 + import { createClientConfig } from '../hey-api'; 3 4 import { 4 5 type ClientOptions as DefaultClientOptions, 5 6 type Config, 6 7 createClient, 7 8 createConfig, 8 - } from '@hey-api/client-next'; 9 - 10 - import { createClientConfig } from '../hey-api'; 9 + } from './client'; 11 10 import type { ClientOptions } from './types.gen'; 12 11 13 12 /**
+163
examples/openapi-ts-next/src/client/client/client.ts
··· 1 + import type { Client, Config, RequestOptions } from './types'; 2 + import { 3 + buildUrl, 4 + createConfig, 5 + createInterceptors, 6 + getParseAs, 7 + mergeConfigs, 8 + mergeHeaders, 9 + setAuthParams, 10 + } from './utils'; 11 + 12 + type ReqInit = Omit<RequestInit, 'body' | 'headers'> & { 13 + body?: any; 14 + headers: ReturnType<typeof mergeHeaders>; 15 + }; 16 + 17 + export const createClient = (config: Config = {}): Client => { 18 + let _config = mergeConfigs(createConfig(), config); 19 + 20 + const getConfig = (): Config => ({ ..._config }); 21 + 22 + const setConfig = (config: Config): Config => { 23 + _config = mergeConfigs(_config, config); 24 + return getConfig(); 25 + }; 26 + 27 + const interceptors = createInterceptors<Response, unknown, RequestOptions>(); 28 + 29 + // @ts-expect-error 30 + const request: Client['request'] = async (options) => { 31 + const opts = { 32 + ..._config, 33 + ...options, 34 + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, 35 + headers: mergeHeaders(_config.headers, options.headers), 36 + }; 37 + 38 + if (opts.security) { 39 + await setAuthParams({ 40 + ...opts, 41 + security: opts.security, 42 + }); 43 + } 44 + 45 + if (opts.body && opts.bodySerializer) { 46 + opts.body = opts.bodySerializer(opts.body); 47 + } 48 + 49 + // remove Content-Type header if body is empty to avoid sending invalid requests 50 + if (opts.body === undefined || opts.body === '') { 51 + opts.headers.delete('Content-Type'); 52 + } 53 + 54 + for (const fn of interceptors.request._fns) { 55 + if (fn) { 56 + await fn(opts); 57 + } 58 + } 59 + 60 + const url = buildUrl(opts); 61 + // fetch must be assigned here, otherwise it would throw the error: 62 + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation 63 + const _fetch = opts.fetch!; 64 + let response = await _fetch(url, { 65 + ...opts, 66 + body: opts.body as ReqInit['body'], 67 + }); 68 + 69 + for (const fn of interceptors.response._fns) { 70 + if (fn) { 71 + response = await fn(response, opts); 72 + } 73 + } 74 + 75 + const result = { 76 + response, 77 + }; 78 + 79 + if (response.ok) { 80 + if ( 81 + response.status === 204 || 82 + response.headers.get('Content-Length') === '0' 83 + ) { 84 + return { 85 + data: {}, 86 + ...result, 87 + }; 88 + } 89 + 90 + const parseAs = 91 + (opts.parseAs === 'auto' 92 + ? getParseAs(response.headers.get('Content-Type')) 93 + : opts.parseAs) ?? 'json'; 94 + 95 + if (parseAs === 'stream') { 96 + return { 97 + data: response.body, 98 + ...result, 99 + }; 100 + } 101 + 102 + let data = await response[parseAs](); 103 + if (parseAs === 'json') { 104 + if (opts.responseValidator) { 105 + await opts.responseValidator(data); 106 + } 107 + 108 + if (opts.responseTransformer) { 109 + data = await opts.responseTransformer(data); 110 + } 111 + } 112 + 113 + return { 114 + data, 115 + ...result, 116 + }; 117 + } 118 + 119 + let error = await response.text(); 120 + 121 + try { 122 + error = JSON.parse(error); 123 + } catch { 124 + // noop 125 + } 126 + 127 + let finalError = error; 128 + 129 + for (const fn of interceptors.error._fns) { 130 + if (fn) { 131 + finalError = (await fn(error, response, opts)) as string; 132 + } 133 + } 134 + 135 + finalError = finalError || ({} as string); 136 + 137 + if (opts.throwOnError) { 138 + throw finalError; 139 + } 140 + 141 + return { 142 + error: finalError, 143 + ...result, 144 + }; 145 + }; 146 + 147 + return { 148 + buildUrl, 149 + connect: (options) => request({ ...options, method: 'CONNECT' }), 150 + delete: (options) => request({ ...options, method: 'DELETE' }), 151 + get: (options) => request({ ...options, method: 'GET' }), 152 + getConfig, 153 + head: (options) => request({ ...options, method: 'HEAD' }), 154 + interceptors, 155 + options: (options) => request({ ...options, method: 'OPTIONS' }), 156 + patch: (options) => request({ ...options, method: 'PATCH' }), 157 + post: (options) => request({ ...options, method: 'POST' }), 158 + put: (options) => request({ ...options, method: 'PUT' }), 159 + request, 160 + setConfig, 161 + trace: (options) => request({ ...options, method: 'TRACE' }), 162 + }; 163 + };
+21
examples/openapi-ts-next/src/client/client/index.ts
··· 1 + export type { Auth } from '../core/auth'; 2 + export type { QuerySerializerOptions } from '../core/bodySerializer'; 3 + export { 4 + formDataBodySerializer, 5 + jsonBodySerializer, 6 + urlSearchParamsBodySerializer, 7 + } from '../core/bodySerializer'; 8 + export { buildClientParams } from '../core/params'; 9 + export { createClient } from './client'; 10 + export type { 11 + Client, 12 + ClientOptions, 13 + Config, 14 + CreateClientConfig, 15 + Options, 16 + OptionsLegacyParser, 17 + RequestOptions, 18 + RequestResult, 19 + TDataShape, 20 + } from './types'; 21 + export { createConfig } from './utils';
+163
examples/openapi-ts-next/src/client/client/types.ts
··· 1 + import type { Auth } from '../core/auth'; 2 + import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; 3 + import type { Middleware } from './utils'; 4 + 5 + export interface Config<T extends ClientOptions = ClientOptions> 6 + extends Omit<RequestInit, 'body' | 'headers' | 'method'>, 7 + CoreConfig { 8 + /** 9 + * Base URL for all requests made by this client. 10 + */ 11 + baseUrl?: T['baseUrl']; 12 + /** 13 + * Fetch API implementation. You can use this option to provide a custom 14 + * fetch instance. 15 + * 16 + * @default globalThis.fetch 17 + */ 18 + fetch?: typeof fetch; 19 + /** 20 + * Return the response data parsed in a specified format. By default, `auto` 21 + * will infer the appropriate method from the `Content-Type` response header. 22 + * You can override this behavior with any of the {@link Body} methods. 23 + * Select `stream` if you don't want to parse response data at all. 24 + * 25 + * @default 'auto' 26 + */ 27 + parseAs?: Exclude<keyof Body, 'body' | 'bodyUsed'> | 'auto' | 'stream'; 28 + /** 29 + * Throw an error instead of returning it in the response? 30 + * 31 + * @default false 32 + */ 33 + throwOnError?: T['throwOnError']; 34 + } 35 + 36 + export interface RequestOptions< 37 + ThrowOnError extends boolean = boolean, 38 + Url extends string = string, 39 + > extends Config<{ 40 + throwOnError: ThrowOnError; 41 + }> { 42 + /** 43 + * Any body that you want to add to your request. 44 + * 45 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} 46 + */ 47 + body?: unknown; 48 + path?: Record<string, unknown>; 49 + query?: Record<string, unknown>; 50 + /** 51 + * Security mechanism(s) to use for the request. 52 + */ 53 + security?: ReadonlyArray<Auth>; 54 + url: Url; 55 + } 56 + 57 + export type RequestResult< 58 + TData = unknown, 59 + TError = unknown, 60 + ThrowOnError extends boolean = boolean, 61 + > = ThrowOnError extends true 62 + ? Promise<{ 63 + data: TData extends Record<string, unknown> ? TData[keyof TData] : TData; 64 + response: Response; 65 + }> 66 + : Promise< 67 + ( 68 + | { 69 + data: TData extends Record<string, unknown> 70 + ? TData[keyof TData] 71 + : TData; 72 + error: undefined; 73 + } 74 + | { 75 + data: undefined; 76 + error: TError extends Record<string, unknown> 77 + ? TError[keyof TError] 78 + : TError; 79 + } 80 + ) & { 81 + response: Response; 82 + } 83 + >; 84 + 85 + export interface ClientOptions { 86 + baseUrl?: string; 87 + throwOnError?: boolean; 88 + } 89 + 90 + type MethodFn = < 91 + TData = unknown, 92 + TError = unknown, 93 + ThrowOnError extends boolean = false, 94 + >( 95 + options: Omit<RequestOptions<ThrowOnError>, 'method'>, 96 + ) => RequestResult<TData, TError, ThrowOnError>; 97 + 98 + type RequestFn = < 99 + TData = unknown, 100 + TError = unknown, 101 + ThrowOnError extends boolean = false, 102 + >( 103 + options: Omit<RequestOptions<ThrowOnError>, 'method'> & 104 + Pick<Required<RequestOptions<ThrowOnError>>, 'method'>, 105 + ) => RequestResult<TData, TError, ThrowOnError>; 106 + 107 + type BuildUrlFn = < 108 + TData extends { 109 + body?: unknown; 110 + path?: Record<string, unknown>; 111 + query?: Record<string, unknown>; 112 + url: string; 113 + }, 114 + >( 115 + options: Pick<TData, 'url'> & Options<TData>, 116 + ) => string; 117 + 118 + export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & { 119 + interceptors: Middleware<Response, unknown, RequestOptions>; 120 + }; 121 + 122 + /** 123 + * The `createClientConfig()` function will be called on client initialization 124 + * and the returned object will become the client's initial configuration. 125 + * 126 + * You may want to initialize your client this way instead of calling 127 + * `setConfig()`. This is useful for example if you're using Next.js 128 + * to ensure your client always has the correct values. 129 + */ 130 + export type CreateClientConfig<T extends ClientOptions = ClientOptions> = ( 131 + override?: Config<ClientOptions & T>, 132 + ) => Config<Required<ClientOptions> & T>; 133 + 134 + export interface TDataShape { 135 + body?: unknown; 136 + headers?: unknown; 137 + path?: unknown; 138 + query?: unknown; 139 + url: string; 140 + } 141 + 142 + type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>; 143 + 144 + export type Options< 145 + TData extends TDataShape = TDataShape, 146 + ThrowOnError extends boolean = boolean, 147 + > = OmitKeys<RequestOptions<ThrowOnError>, 'body' | 'path' | 'query' | 'url'> & 148 + Omit<TData, 'url'>; 149 + 150 + export type OptionsLegacyParser< 151 + TData = unknown, 152 + ThrowOnError extends boolean = boolean, 153 + > = TData extends { body?: any } 154 + ? TData extends { headers?: any } 155 + ? OmitKeys<RequestOptions<ThrowOnError>, 'body' | 'headers' | 'url'> & TData 156 + : OmitKeys<RequestOptions<ThrowOnError>, 'body' | 'url'> & 157 + TData & 158 + Pick<RequestOptions<ThrowOnError>, 'headers'> 159 + : TData extends { headers?: any } 160 + ? OmitKeys<RequestOptions<ThrowOnError>, 'headers' | 'url'> & 161 + TData & 162 + Pick<RequestOptions<ThrowOnError>, 'body'> 163 + : OmitKeys<RequestOptions<ThrowOnError>, 'url'> & TData;
+404
examples/openapi-ts-next/src/client/client/utils.ts
··· 1 + import { getAuthToken } from '../core/auth'; 2 + import type { 3 + QuerySerializer, 4 + QuerySerializerOptions, 5 + } from '../core/bodySerializer'; 6 + import { jsonBodySerializer } from '../core/bodySerializer'; 7 + import { 8 + serializeArrayParam, 9 + serializeObjectParam, 10 + serializePrimitiveParam, 11 + } from '../core/pathSerializer'; 12 + import type { Client, ClientOptions, Config, RequestOptions } from './types'; 13 + 14 + interface PathSerializer { 15 + path: Record<string, unknown>; 16 + url: string; 17 + } 18 + 19 + const PATH_PARAM_RE = /\{[^{}]+\}/g; 20 + 21 + type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; 22 + type MatrixStyle = 'label' | 'matrix' | 'simple'; 23 + type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 24 + 25 + const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { 26 + let url = _url; 27 + const matches = _url.match(PATH_PARAM_RE); 28 + if (matches) { 29 + for (const match of matches) { 30 + let explode = false; 31 + let name = match.substring(1, match.length - 1); 32 + let style: ArraySeparatorStyle = 'simple'; 33 + 34 + if (name.endsWith('*')) { 35 + explode = true; 36 + name = name.substring(0, name.length - 1); 37 + } 38 + 39 + if (name.startsWith('.')) { 40 + name = name.substring(1); 41 + style = 'label'; 42 + } else if (name.startsWith(';')) { 43 + name = name.substring(1); 44 + style = 'matrix'; 45 + } 46 + 47 + const value = path[name]; 48 + 49 + if (value === undefined || value === null) { 50 + continue; 51 + } 52 + 53 + if (Array.isArray(value)) { 54 + url = url.replace( 55 + match, 56 + serializeArrayParam({ explode, name, style, value }), 57 + ); 58 + continue; 59 + } 60 + 61 + if (typeof value === 'object') { 62 + url = url.replace( 63 + match, 64 + serializeObjectParam({ 65 + explode, 66 + name, 67 + style, 68 + value: value as Record<string, unknown>, 69 + valueOnly: true, 70 + }), 71 + ); 72 + continue; 73 + } 74 + 75 + if (style === 'matrix') { 76 + url = url.replace( 77 + match, 78 + `;${serializePrimitiveParam({ 79 + name, 80 + value: value as string, 81 + })}`, 82 + ); 83 + continue; 84 + } 85 + 86 + const replaceValue = encodeURIComponent( 87 + style === 'label' ? `.${value as string}` : (value as string), 88 + ); 89 + url = url.replace(match, replaceValue); 90 + } 91 + } 92 + return url; 93 + }; 94 + 95 + export const createQuerySerializer = <T = unknown>({ 96 + allowReserved, 97 + array, 98 + object, 99 + }: QuerySerializerOptions = {}) => { 100 + const querySerializer = (queryParams: T) => { 101 + const search: string[] = []; 102 + if (queryParams && typeof queryParams === 'object') { 103 + for (const name in queryParams) { 104 + const value = queryParams[name]; 105 + 106 + if (value === undefined || value === null) { 107 + continue; 108 + } 109 + 110 + if (Array.isArray(value)) { 111 + const serializedArray = serializeArrayParam({ 112 + allowReserved, 113 + explode: true, 114 + name, 115 + style: 'form', 116 + value, 117 + ...array, 118 + }); 119 + if (serializedArray) search.push(serializedArray); 120 + } else if (typeof value === 'object') { 121 + const serializedObject = serializeObjectParam({ 122 + allowReserved, 123 + explode: true, 124 + name, 125 + style: 'deepObject', 126 + value: value as Record<string, unknown>, 127 + ...object, 128 + }); 129 + if (serializedObject) search.push(serializedObject); 130 + } else { 131 + const serializedPrimitive = serializePrimitiveParam({ 132 + allowReserved, 133 + name, 134 + value: value as string, 135 + }); 136 + if (serializedPrimitive) search.push(serializedPrimitive); 137 + } 138 + } 139 + } 140 + return search.join('&'); 141 + }; 142 + return querySerializer; 143 + }; 144 + 145 + /** 146 + * Infers parseAs value from provided Content-Type header. 147 + */ 148 + export const getParseAs = ( 149 + contentType: string | null, 150 + ): Exclude<Config['parseAs'], 'auto'> => { 151 + if (!contentType) { 152 + // If no Content-Type header is provided, the best we can do is return the raw response body, 153 + // which is effectively the same as the 'stream' option. 154 + return 'stream'; 155 + } 156 + 157 + const cleanContent = contentType.split(';')[0]?.trim(); 158 + 159 + if (!cleanContent) { 160 + return; 161 + } 162 + 163 + if ( 164 + cleanContent.startsWith('application/json') || 165 + cleanContent.endsWith('+json') 166 + ) { 167 + return 'json'; 168 + } 169 + 170 + if (cleanContent === 'multipart/form-data') { 171 + return 'formData'; 172 + } 173 + 174 + if ( 175 + ['application/', 'audio/', 'image/', 'video/'].some((type) => 176 + cleanContent.startsWith(type), 177 + ) 178 + ) { 179 + return 'blob'; 180 + } 181 + 182 + if (cleanContent.startsWith('text/')) { 183 + return 'text'; 184 + } 185 + }; 186 + 187 + export const setAuthParams = async ({ 188 + security, 189 + ...options 190 + }: Pick<Required<RequestOptions>, 'security'> & 191 + Pick<RequestOptions, 'auth' | 'query'> & { 192 + headers: Headers; 193 + }) => { 194 + for (const auth of security) { 195 + const token = await getAuthToken(auth, options.auth); 196 + 197 + if (!token) { 198 + continue; 199 + } 200 + 201 + const name = auth.name ?? 'Authorization'; 202 + 203 + switch (auth.in) { 204 + case 'query': 205 + if (!options.query) { 206 + options.query = {}; 207 + } 208 + options.query[name] = token; 209 + break; 210 + case 'cookie': 211 + options.headers.append('Cookie', `${name}=${token}`); 212 + break; 213 + case 'header': 214 + default: 215 + options.headers.set(name, token); 216 + break; 217 + } 218 + 219 + return; 220 + } 221 + }; 222 + 223 + export const buildUrl: Client['buildUrl'] = (options) => { 224 + const url = getUrl({ 225 + baseUrl: options.baseUrl as string, 226 + path: options.path, 227 + query: options.query, 228 + querySerializer: 229 + typeof options.querySerializer === 'function' 230 + ? options.querySerializer 231 + : createQuerySerializer(options.querySerializer), 232 + url: options.url, 233 + }); 234 + return url; 235 + }; 236 + 237 + export const getUrl = ({ 238 + baseUrl, 239 + path, 240 + query, 241 + querySerializer, 242 + url: _url, 243 + }: { 244 + baseUrl?: string; 245 + path?: Record<string, unknown>; 246 + query?: Record<string, unknown>; 247 + querySerializer: QuerySerializer; 248 + url: string; 249 + }) => { 250 + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; 251 + let url = (baseUrl ?? '') + pathUrl; 252 + if (path) { 253 + url = defaultPathSerializer({ path, url }); 254 + } 255 + let search = query ? querySerializer(query) : ''; 256 + if (search.startsWith('?')) { 257 + search = search.substring(1); 258 + } 259 + if (search) { 260 + url += `?${search}`; 261 + } 262 + return url; 263 + }; 264 + 265 + export const mergeConfigs = (a: Config, b: Config): Config => { 266 + const config = { ...a, ...b }; 267 + if (config.baseUrl?.endsWith('/')) { 268 + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); 269 + } 270 + config.headers = mergeHeaders(a.headers, b.headers); 271 + return config; 272 + }; 273 + 274 + export const mergeHeaders = ( 275 + ...headers: Array<Required<Config>['headers'] | undefined> 276 + ): Headers => { 277 + const mergedHeaders = new Headers(); 278 + for (const header of headers) { 279 + if (!header || typeof header !== 'object') { 280 + continue; 281 + } 282 + 283 + const iterator = 284 + header instanceof Headers ? header.entries() : Object.entries(header); 285 + 286 + for (const [key, value] of iterator) { 287 + if (value === null) { 288 + mergedHeaders.delete(key); 289 + } else if (Array.isArray(value)) { 290 + for (const v of value) { 291 + mergedHeaders.append(key, v as string); 292 + } 293 + } else if (value !== undefined) { 294 + // assume object headers are meant to be JSON stringified, i.e. their 295 + // content value in OpenAPI specification is 'application/json' 296 + mergedHeaders.set( 297 + key, 298 + typeof value === 'object' ? JSON.stringify(value) : (value as string), 299 + ); 300 + } 301 + } 302 + } 303 + return mergedHeaders; 304 + }; 305 + 306 + type ErrInterceptor<Err, Res, Options> = ( 307 + error: Err, 308 + response: Res, 309 + options: Options, 310 + ) => Err | Promise<Err>; 311 + 312 + type ReqInterceptor<Options> = (options: Options) => void | Promise<void>; 313 + 314 + type ResInterceptor<Res, Options> = ( 315 + response: Res, 316 + options: Options, 317 + ) => Res | Promise<Res>; 318 + 319 + class Interceptors<Interceptor> { 320 + _fns: (Interceptor | null)[]; 321 + 322 + constructor() { 323 + this._fns = []; 324 + } 325 + 326 + clear() { 327 + this._fns = []; 328 + } 329 + 330 + getInterceptorIndex(id: number | Interceptor): number { 331 + if (typeof id === 'number') { 332 + return this._fns[id] ? id : -1; 333 + } else { 334 + return this._fns.indexOf(id); 335 + } 336 + } 337 + exists(id: number | Interceptor) { 338 + const index = this.getInterceptorIndex(id); 339 + return !!this._fns[index]; 340 + } 341 + 342 + eject(id: number | Interceptor) { 343 + const index = this.getInterceptorIndex(id); 344 + if (this._fns[index]) { 345 + this._fns[index] = null; 346 + } 347 + } 348 + 349 + update(id: number | Interceptor, fn: Interceptor) { 350 + const index = this.getInterceptorIndex(id); 351 + if (this._fns[index]) { 352 + this._fns[index] = fn; 353 + return id; 354 + } else { 355 + return false; 356 + } 357 + } 358 + 359 + use(fn: Interceptor) { 360 + this._fns = [...this._fns, fn]; 361 + return this._fns.length - 1; 362 + } 363 + } 364 + 365 + // `createInterceptors()` response, meant for external use as it does not 366 + // expose internals 367 + export interface Middleware<Res, Err, Options> { 368 + error: Pick<Interceptors<ErrInterceptor<Err, Res, Options>>, 'eject' | 'use'>; 369 + request: Pick<Interceptors<ReqInterceptor<Options>>, 'eject' | 'use'>; 370 + response: Pick<Interceptors<ResInterceptor<Res, Options>>, 'eject' | 'use'>; 371 + } 372 + 373 + // do not add `Middleware` as return type so we can use _fns internally 374 + export const createInterceptors = <Res, Err, Options>() => ({ 375 + error: new Interceptors<ErrInterceptor<Err, Res, Options>>(), 376 + request: new Interceptors<ReqInterceptor<Options>>(), 377 + response: new Interceptors<ResInterceptor<Res, Options>>(), 378 + }); 379 + 380 + const defaultQuerySerializer = createQuerySerializer({ 381 + allowReserved: false, 382 + array: { 383 + explode: true, 384 + style: 'form', 385 + }, 386 + object: { 387 + explode: true, 388 + style: 'deepObject', 389 + }, 390 + }); 391 + 392 + const defaultHeaders = { 393 + 'Content-Type': 'application/json', 394 + }; 395 + 396 + export const createConfig = <T extends ClientOptions = ClientOptions>( 397 + override: Config<Omit<ClientOptions, keyof T> & T> = {}, 398 + ): Config<Omit<ClientOptions, keyof T> & T> => ({ 399 + ...jsonBodySerializer, 400 + headers: defaultHeaders, 401 + parseAs: 'auto', 402 + querySerializer: defaultQuerySerializer, 403 + ...override, 404 + });
+40
examples/openapi-ts-next/src/client/core/auth.ts
··· 1 + export type AuthToken = string | undefined; 2 + 3 + export interface Auth { 4 + /** 5 + * Which part of the request do we use to send the auth? 6 + * 7 + * @default 'header' 8 + */ 9 + in?: 'header' | 'query' | 'cookie'; 10 + /** 11 + * Header or query parameter name. 12 + * 13 + * @default 'Authorization' 14 + */ 15 + name?: string; 16 + scheme?: 'basic' | 'bearer'; 17 + type: 'apiKey' | 'http'; 18 + } 19 + 20 + export const getAuthToken = async ( 21 + auth: Auth, 22 + callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken, 23 + ): Promise<string | undefined> => { 24 + const token = 25 + typeof callback === 'function' ? await callback(auth) : callback; 26 + 27 + if (!token) { 28 + return; 29 + } 30 + 31 + if (auth.scheme === 'bearer') { 32 + return `Bearer ${token}`; 33 + } 34 + 35 + if (auth.scheme === 'basic') { 36 + return `Basic ${btoa(token)}`; 37 + } 38 + 39 + return token; 40 + };
+84
examples/openapi-ts-next/src/client/core/bodySerializer.ts
··· 1 + import type { 2 + ArrayStyle, 3 + ObjectStyle, 4 + SerializerOptions, 5 + } from './pathSerializer'; 6 + 7 + export type QuerySerializer = (query: Record<string, unknown>) => string; 8 + 9 + export type BodySerializer = (body: any) => any; 10 + 11 + export interface QuerySerializerOptions { 12 + allowReserved?: boolean; 13 + array?: SerializerOptions<ArrayStyle>; 14 + object?: SerializerOptions<ObjectStyle>; 15 + } 16 + 17 + const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { 18 + if (typeof value === 'string' || value instanceof Blob) { 19 + data.append(key, value); 20 + } else { 21 + data.append(key, JSON.stringify(value)); 22 + } 23 + }; 24 + 25 + const serializeUrlSearchParamsPair = ( 26 + data: URLSearchParams, 27 + key: string, 28 + value: unknown, 29 + ) => { 30 + if (typeof value === 'string') { 31 + data.append(key, value); 32 + } else { 33 + data.append(key, JSON.stringify(value)); 34 + } 35 + }; 36 + 37 + export const formDataBodySerializer = { 38 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 39 + body: T, 40 + ) => { 41 + const data = new FormData(); 42 + 43 + Object.entries(body).forEach(([key, value]) => { 44 + if (value === undefined || value === null) { 45 + return; 46 + } 47 + if (Array.isArray(value)) { 48 + value.forEach((v) => serializeFormDataPair(data, key, v)); 49 + } else { 50 + serializeFormDataPair(data, key, value); 51 + } 52 + }); 53 + 54 + return data; 55 + }, 56 + }; 57 + 58 + export const jsonBodySerializer = { 59 + bodySerializer: <T>(body: T) => 60 + JSON.stringify(body, (key, value) => 61 + typeof value === 'bigint' ? value.toString() : value, 62 + ), 63 + }; 64 + 65 + export const urlSearchParamsBodySerializer = { 66 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 67 + body: T, 68 + ) => { 69 + const data = new URLSearchParams(); 70 + 71 + Object.entries(body).forEach(([key, value]) => { 72 + if (value === undefined || value === null) { 73 + return; 74 + } 75 + if (Array.isArray(value)) { 76 + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); 77 + } else { 78 + serializeUrlSearchParamsPair(data, key, value); 79 + } 80 + }); 81 + 82 + return data.toString(); 83 + }, 84 + };
+141
examples/openapi-ts-next/src/client/core/params.ts
··· 1 + type Slot = 'body' | 'headers' | 'path' | 'query'; 2 + 3 + export type Field = 4 + | { 5 + in: Exclude<Slot, 'body'>; 6 + key: string; 7 + map?: string; 8 + } 9 + | { 10 + in: Extract<Slot, 'body'>; 11 + key?: string; 12 + map?: string; 13 + }; 14 + 15 + export interface Fields { 16 + allowExtra?: Partial<Record<Slot, boolean>>; 17 + args?: ReadonlyArray<Field>; 18 + } 19 + 20 + export type FieldsConfig = ReadonlyArray<Field | Fields>; 21 + 22 + const extraPrefixesMap: Record<string, Slot> = { 23 + $body_: 'body', 24 + $headers_: 'headers', 25 + $path_: 'path', 26 + $query_: 'query', 27 + }; 28 + const extraPrefixes = Object.entries(extraPrefixesMap); 29 + 30 + type KeyMap = Map< 31 + string, 32 + { 33 + in: Slot; 34 + map?: string; 35 + } 36 + >; 37 + 38 + const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { 39 + if (!map) { 40 + map = new Map(); 41 + } 42 + 43 + for (const config of fields) { 44 + if ('in' in config) { 45 + if (config.key) { 46 + map.set(config.key, { 47 + in: config.in, 48 + map: config.map, 49 + }); 50 + } 51 + } else if (config.args) { 52 + buildKeyMap(config.args, map); 53 + } 54 + } 55 + 56 + return map; 57 + }; 58 + 59 + interface Params { 60 + body: unknown; 61 + headers: Record<string, unknown>; 62 + path: Record<string, unknown>; 63 + query: Record<string, unknown>; 64 + } 65 + 66 + const stripEmptySlots = (params: Params) => { 67 + for (const [slot, value] of Object.entries(params)) { 68 + if (value && typeof value === 'object' && !Object.keys(value).length) { 69 + delete params[slot as Slot]; 70 + } 71 + } 72 + }; 73 + 74 + export const buildClientParams = ( 75 + args: ReadonlyArray<unknown>, 76 + fields: FieldsConfig, 77 + ) => { 78 + const params: Params = { 79 + body: {}, 80 + headers: {}, 81 + path: {}, 82 + query: {}, 83 + }; 84 + 85 + const map = buildKeyMap(fields); 86 + 87 + let config: FieldsConfig[number] | undefined; 88 + 89 + for (const [index, arg] of args.entries()) { 90 + if (fields[index]) { 91 + config = fields[index]; 92 + } 93 + 94 + if (!config) { 95 + continue; 96 + } 97 + 98 + if ('in' in config) { 99 + if (config.key) { 100 + const field = map.get(config.key)!; 101 + const name = field.map || config.key; 102 + (params[field.in] as Record<string, unknown>)[name] = arg; 103 + } else { 104 + params.body = arg; 105 + } 106 + } else { 107 + for (const [key, value] of Object.entries(arg ?? {})) { 108 + const field = map.get(key); 109 + 110 + if (field) { 111 + const name = field.map || key; 112 + (params[field.in] as Record<string, unknown>)[name] = value; 113 + } else { 114 + const extra = extraPrefixes.find(([prefix]) => 115 + key.startsWith(prefix), 116 + ); 117 + 118 + if (extra) { 119 + const [prefix, slot] = extra; 120 + (params[slot] as Record<string, unknown>)[ 121 + key.slice(prefix.length) 122 + ] = value; 123 + } else { 124 + for (const [slot, allowed] of Object.entries( 125 + config.allowExtra ?? {}, 126 + )) { 127 + if (allowed) { 128 + (params[slot as Slot] as Record<string, unknown>)[key] = value; 129 + break; 130 + } 131 + } 132 + } 133 + } 134 + } 135 + } 136 + } 137 + 138 + stripEmptySlots(params); 139 + 140 + return params; 141 + };
+179
examples/openapi-ts-next/src/client/core/pathSerializer.ts
··· 1 + interface SerializeOptions<T> 2 + extends SerializePrimitiveOptions, 3 + SerializerOptions<T> {} 4 + 5 + interface SerializePrimitiveOptions { 6 + allowReserved?: boolean; 7 + name: string; 8 + } 9 + 10 + export interface SerializerOptions<T> { 11 + /** 12 + * @default true 13 + */ 14 + explode: boolean; 15 + style: T; 16 + } 17 + 18 + export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; 19 + export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 20 + type MatrixStyle = 'label' | 'matrix' | 'simple'; 21 + export type ObjectStyle = 'form' | 'deepObject'; 22 + type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; 23 + 24 + interface SerializePrimitiveParam extends SerializePrimitiveOptions { 25 + value: string; 26 + } 27 + 28 + export const separatorArrayExplode = (style: ArraySeparatorStyle) => { 29 + switch (style) { 30 + case 'label': 31 + return '.'; 32 + case 'matrix': 33 + return ';'; 34 + case 'simple': 35 + return ','; 36 + default: 37 + return '&'; 38 + } 39 + }; 40 + 41 + export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { 42 + switch (style) { 43 + case 'form': 44 + return ','; 45 + case 'pipeDelimited': 46 + return '|'; 47 + case 'spaceDelimited': 48 + return '%20'; 49 + default: 50 + return ','; 51 + } 52 + }; 53 + 54 + export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { 55 + switch (style) { 56 + case 'label': 57 + return '.'; 58 + case 'matrix': 59 + return ';'; 60 + case 'simple': 61 + return ','; 62 + default: 63 + return '&'; 64 + } 65 + }; 66 + 67 + export const serializeArrayParam = ({ 68 + allowReserved, 69 + explode, 70 + name, 71 + style, 72 + value, 73 + }: SerializeOptions<ArraySeparatorStyle> & { 74 + value: unknown[]; 75 + }) => { 76 + if (!explode) { 77 + const joinedValues = ( 78 + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) 79 + ).join(separatorArrayNoExplode(style)); 80 + switch (style) { 81 + case 'label': 82 + return `.${joinedValues}`; 83 + case 'matrix': 84 + return `;${name}=${joinedValues}`; 85 + case 'simple': 86 + return joinedValues; 87 + default: 88 + return `${name}=${joinedValues}`; 89 + } 90 + } 91 + 92 + const separator = separatorArrayExplode(style); 93 + const joinedValues = value 94 + .map((v) => { 95 + if (style === 'label' || style === 'simple') { 96 + return allowReserved ? v : encodeURIComponent(v as string); 97 + } 98 + 99 + return serializePrimitiveParam({ 100 + allowReserved, 101 + name, 102 + value: v as string, 103 + }); 104 + }) 105 + .join(separator); 106 + return style === 'label' || style === 'matrix' 107 + ? separator + joinedValues 108 + : joinedValues; 109 + }; 110 + 111 + export const serializePrimitiveParam = ({ 112 + allowReserved, 113 + name, 114 + value, 115 + }: SerializePrimitiveParam) => { 116 + if (value === undefined || value === null) { 117 + return ''; 118 + } 119 + 120 + if (typeof value === 'object') { 121 + throw new Error( 122 + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', 123 + ); 124 + } 125 + 126 + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; 127 + }; 128 + 129 + export const serializeObjectParam = ({ 130 + allowReserved, 131 + explode, 132 + name, 133 + style, 134 + value, 135 + valueOnly, 136 + }: SerializeOptions<ObjectSeparatorStyle> & { 137 + value: Record<string, unknown> | Date; 138 + valueOnly?: boolean; 139 + }) => { 140 + if (value instanceof Date) { 141 + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; 142 + } 143 + 144 + if (style !== 'deepObject' && !explode) { 145 + let values: string[] = []; 146 + Object.entries(value).forEach(([key, v]) => { 147 + values = [ 148 + ...values, 149 + key, 150 + allowReserved ? (v as string) : encodeURIComponent(v as string), 151 + ]; 152 + }); 153 + const joinedValues = values.join(','); 154 + switch (style) { 155 + case 'form': 156 + return `${name}=${joinedValues}`; 157 + case 'label': 158 + return `.${joinedValues}`; 159 + case 'matrix': 160 + return `;${name}=${joinedValues}`; 161 + default: 162 + return joinedValues; 163 + } 164 + } 165 + 166 + const separator = separatorObjectExplode(style); 167 + const joinedValues = Object.entries(value) 168 + .map(([key, v]) => 169 + serializePrimitiveParam({ 170 + allowReserved, 171 + name: style === 'deepObject' ? `${name}[${key}]` : key, 172 + value: v as string, 173 + }), 174 + ) 175 + .join(separator); 176 + return style === 'label' || style === 'matrix' 177 + ? separator + joinedValues 178 + : joinedValues; 179 + };
+98
examples/openapi-ts-next/src/client/core/types.ts
··· 1 + import type { Auth, AuthToken } from './auth'; 2 + import type { 3 + BodySerializer, 4 + QuerySerializer, 5 + QuerySerializerOptions, 6 + } from './bodySerializer'; 7 + 8 + export interface Client< 9 + RequestFn = never, 10 + Config = unknown, 11 + MethodFn = never, 12 + BuildUrlFn = never, 13 + > { 14 + /** 15 + * Returns the final request URL. 16 + */ 17 + buildUrl: BuildUrlFn; 18 + connect: MethodFn; 19 + delete: MethodFn; 20 + get: MethodFn; 21 + getConfig: () => Config; 22 + head: MethodFn; 23 + options: MethodFn; 24 + patch: MethodFn; 25 + post: MethodFn; 26 + put: MethodFn; 27 + request: RequestFn; 28 + setConfig: (config: Config) => Config; 29 + trace: MethodFn; 30 + } 31 + 32 + export interface Config { 33 + /** 34 + * Auth token or a function returning auth token. The resolved value will be 35 + * added to the request payload as defined by its `security` array. 36 + */ 37 + auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken; 38 + /** 39 + * A function for serializing request body parameter. By default, 40 + * {@link JSON.stringify()} will be used. 41 + */ 42 + bodySerializer?: BodySerializer | null; 43 + /** 44 + * An object containing any HTTP headers that you want to pre-populate your 45 + * `Headers` object with. 46 + * 47 + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} 48 + */ 49 + headers?: 50 + | RequestInit['headers'] 51 + | Record< 52 + string, 53 + | string 54 + | number 55 + | boolean 56 + | (string | number | boolean)[] 57 + | null 58 + | undefined 59 + | unknown 60 + >; 61 + /** 62 + * The request method. 63 + * 64 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} 65 + */ 66 + method?: 67 + | 'CONNECT' 68 + | 'DELETE' 69 + | 'GET' 70 + | 'HEAD' 71 + | 'OPTIONS' 72 + | 'PATCH' 73 + | 'POST' 74 + | 'PUT' 75 + | 'TRACE'; 76 + /** 77 + * A function for serializing request query parameters. By default, arrays 78 + * will be exploded in form style, objects will be exploded in deepObject 79 + * style, and reserved characters are percent-encoded. 80 + * 81 + * This method will have no effect if the native `paramsSerializer()` Axios 82 + * API function is used. 83 + * 84 + * {@link https://swagger.io/docs/specification/serialization/#query View examples} 85 + */ 86 + querySerializer?: QuerySerializer | QuerySerializerOptions; 87 + /** 88 + * A function transforming response data before it's returned. This is useful 89 + * for post-processing data, e.g. converting ISO strings into Date objects. 90 + */ 91 + responseTransformer?: (data: unknown) => Promise<unknown>; 92 + /** 93 + * A function validating response data. This is useful if you want to ensure 94 + * the response conforms to the desired shape, so it can be safely passed to 95 + * the transformers and returned to the user. 96 + */ 97 + responseValidator?: (data: unknown) => Promise<unknown>; 98 + }
+5 -10
examples/openapi-ts-next/src/client/sdk.gen.ts
··· 1 1 // This file is auto-generated by @hey-api/openapi-ts 2 2 3 - import type { 4 - Client, 5 - Options as ClientOptions, 6 - TDataShape, 7 - } from '@hey-api/client-next'; 8 - 3 + import type { Client, Options as ClientOptions, TDataShape } from './client'; 9 4 import { client as _heyApiClient } from './client.gen'; 10 5 import type { 11 6 AddPetData, ··· 106 101 ...options, 107 102 headers: { 108 103 'Content-Type': 'application/json', 109 - ...options?.headers, 104 + ...options.headers, 110 105 }, 111 106 }); 112 107 ··· 132 127 ...options, 133 128 headers: { 134 129 'Content-Type': 'application/json', 135 - ...options?.headers, 130 + ...options.headers, 136 131 }, 137 132 }); 138 133 ··· 273 268 ...options, 274 269 headers: { 275 270 'Content-Type': 'application/octet-stream', 276 - ...options?.headers, 271 + ...options.headers, 277 272 }, 278 273 }); 279 274 ··· 471 466 ...options, 472 467 headers: { 473 468 'Content-Type': 'application/json', 474 - ...options?.headers, 469 + ...options.headers, 475 470 }, 476 471 });
-1
examples/openapi-ts-sample/package.json
··· 12 12 "typecheck": "tsc --noEmit" 13 13 }, 14 14 "dependencies": { 15 - "@hey-api/client-fetch": "workspace:*", 16 15 "@radix-ui/react-form": "0.1.1", 17 16 "@radix-ui/react-icons": "1.3.2", 18 17 "@radix-ui/themes": "3.1.6",
+2 -3
examples/openapi-ts-sample/src/client/client.gen.ts
··· 1 1 // This file is auto-generated by @hey-api/openapi-ts 2 2 3 + import { createClientConfig } from '../hey-api'; 3 4 import { 4 5 type ClientOptions as DefaultClientOptions, 5 6 type Config, 6 7 createClient, 7 8 createConfig, 8 - } from '@hey-api/client-fetch'; 9 - 10 - import { createClientConfig } from '../hey-api'; 9 + } from './client'; 11 10 import type { ClientOptions } from './types.gen'; 12 11 13 12 /**
+181
examples/openapi-ts-sample/src/client/client/client.ts
··· 1 + import type { Client, Config, RequestOptions } from './types'; 2 + import { 3 + buildUrl, 4 + createConfig, 5 + createInterceptors, 6 + getParseAs, 7 + mergeConfigs, 8 + mergeHeaders, 9 + setAuthParams, 10 + } from './utils'; 11 + 12 + type ReqInit = Omit<RequestInit, 'body' | 'headers'> & { 13 + body?: any; 14 + headers: ReturnType<typeof mergeHeaders>; 15 + }; 16 + 17 + export const createClient = (config: Config = {}): Client => { 18 + let _config = mergeConfigs(createConfig(), config); 19 + 20 + const getConfig = (): Config => ({ ..._config }); 21 + 22 + const setConfig = (config: Config): Config => { 23 + _config = mergeConfigs(_config, config); 24 + return getConfig(); 25 + }; 26 + 27 + const interceptors = createInterceptors< 28 + Request, 29 + Response, 30 + unknown, 31 + RequestOptions 32 + >(); 33 + 34 + const request: Client['request'] = async (options) => { 35 + const opts = { 36 + ..._config, 37 + ...options, 38 + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, 39 + headers: mergeHeaders(_config.headers, options.headers), 40 + }; 41 + 42 + if (opts.security) { 43 + await setAuthParams({ 44 + ...opts, 45 + security: opts.security, 46 + }); 47 + } 48 + 49 + if (opts.body && opts.bodySerializer) { 50 + opts.body = opts.bodySerializer(opts.body); 51 + } 52 + 53 + // remove Content-Type header if body is empty to avoid sending invalid requests 54 + if (opts.body === undefined || opts.body === '') { 55 + opts.headers.delete('Content-Type'); 56 + } 57 + 58 + const url = buildUrl(opts); 59 + const requestInit: ReqInit = { 60 + redirect: 'follow', 61 + ...opts, 62 + }; 63 + 64 + let request = new Request(url, requestInit); 65 + 66 + for (const fn of interceptors.request._fns) { 67 + if (fn) { 68 + request = await fn(request, opts); 69 + } 70 + } 71 + 72 + // fetch must be assigned here, otherwise it would throw the error: 73 + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation 74 + const _fetch = opts.fetch!; 75 + let response = await _fetch(request); 76 + 77 + for (const fn of interceptors.response._fns) { 78 + if (fn) { 79 + response = await fn(response, request, opts); 80 + } 81 + } 82 + 83 + const result = { 84 + request, 85 + response, 86 + }; 87 + 88 + if (response.ok) { 89 + if ( 90 + response.status === 204 || 91 + response.headers.get('Content-Length') === '0' 92 + ) { 93 + return opts.responseStyle === 'data' 94 + ? {} 95 + : { 96 + data: {}, 97 + ...result, 98 + }; 99 + } 100 + 101 + const parseAs = 102 + (opts.parseAs === 'auto' 103 + ? getParseAs(response.headers.get('Content-Type')) 104 + : opts.parseAs) ?? 'json'; 105 + 106 + if (parseAs === 'stream') { 107 + return opts.responseStyle === 'data' 108 + ? response.body 109 + : { 110 + data: response.body, 111 + ...result, 112 + }; 113 + } 114 + 115 + let data = await response[parseAs](); 116 + if (parseAs === 'json') { 117 + if (opts.responseValidator) { 118 + await opts.responseValidator(data); 119 + } 120 + 121 + if (opts.responseTransformer) { 122 + data = await opts.responseTransformer(data); 123 + } 124 + } 125 + 126 + return opts.responseStyle === 'data' 127 + ? data 128 + : { 129 + data, 130 + ...result, 131 + }; 132 + } 133 + 134 + let error = await response.text(); 135 + 136 + try { 137 + error = JSON.parse(error); 138 + } catch { 139 + // noop 140 + } 141 + 142 + let finalError = error; 143 + 144 + for (const fn of interceptors.error._fns) { 145 + if (fn) { 146 + finalError = (await fn(error, response, request, opts)) as string; 147 + } 148 + } 149 + 150 + finalError = finalError || ({} as string); 151 + 152 + if (opts.throwOnError) { 153 + throw finalError; 154 + } 155 + 156 + // TODO: we probably want to return error and improve types 157 + return opts.responseStyle === 'data' 158 + ? undefined 159 + : { 160 + error: finalError, 161 + ...result, 162 + }; 163 + }; 164 + 165 + return { 166 + buildUrl, 167 + connect: (options) => request({ ...options, method: 'CONNECT' }), 168 + delete: (options) => request({ ...options, method: 'DELETE' }), 169 + get: (options) => request({ ...options, method: 'GET' }), 170 + getConfig, 171 + head: (options) => request({ ...options, method: 'HEAD' }), 172 + interceptors, 173 + options: (options) => request({ ...options, method: 'OPTIONS' }), 174 + patch: (options) => request({ ...options, method: 'PATCH' }), 175 + post: (options) => request({ ...options, method: 'POST' }), 176 + put: (options) => request({ ...options, method: 'PUT' }), 177 + request, 178 + setConfig, 179 + trace: (options) => request({ ...options, method: 'TRACE' }), 180 + }; 181 + };
+22
examples/openapi-ts-sample/src/client/client/index.ts
··· 1 + export type { Auth } from '../core/auth'; 2 + export type { QuerySerializerOptions } from '../core/bodySerializer'; 3 + export { 4 + formDataBodySerializer, 5 + jsonBodySerializer, 6 + urlSearchParamsBodySerializer, 7 + } from '../core/bodySerializer'; 8 + export { buildClientParams } from '../core/params'; 9 + export { createClient } from './client'; 10 + export type { 11 + Client, 12 + ClientOptions, 13 + Config, 14 + CreateClientConfig, 15 + Options, 16 + OptionsLegacyParser, 17 + RequestOptions, 18 + RequestResult, 19 + ResponseStyle, 20 + TDataShape, 21 + } from './types'; 22 + export { createConfig, mergeHeaders } from './utils';
+212
examples/openapi-ts-sample/src/client/client/types.ts
··· 1 + import type { Auth } from '../core/auth'; 2 + import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; 3 + import type { Middleware } from './utils'; 4 + 5 + export type ResponseStyle = 'data' | 'fields'; 6 + 7 + export interface Config<T extends ClientOptions = ClientOptions> 8 + extends Omit<RequestInit, 'body' | 'headers' | 'method'>, 9 + CoreConfig { 10 + /** 11 + * Base URL for all requests made by this client. 12 + */ 13 + baseUrl?: T['baseUrl']; 14 + /** 15 + * Fetch API implementation. You can use this option to provide a custom 16 + * fetch instance. 17 + * 18 + * @default globalThis.fetch 19 + */ 20 + fetch?: (request: Request) => ReturnType<typeof fetch>; 21 + /** 22 + * Please don't use the Fetch client for Next.js applications. The `next` 23 + * options won't have any effect. 24 + * 25 + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. 26 + */ 27 + next?: never; 28 + /** 29 + * Return the response data parsed in a specified format. By default, `auto` 30 + * will infer the appropriate method from the `Content-Type` response header. 31 + * You can override this behavior with any of the {@link Body} methods. 32 + * Select `stream` if you don't want to parse response data at all. 33 + * 34 + * @default 'auto' 35 + */ 36 + parseAs?: Exclude<keyof Body, 'body' | 'bodyUsed'> | 'auto' | 'stream'; 37 + /** 38 + * Should we return only data or multiple fields (data, error, response, etc.)? 39 + * 40 + * @default 'fields' 41 + */ 42 + responseStyle?: ResponseStyle; 43 + /** 44 + * Throw an error instead of returning it in the response? 45 + * 46 + * @default false 47 + */ 48 + throwOnError?: T['throwOnError']; 49 + } 50 + 51 + export interface RequestOptions< 52 + TResponseStyle extends ResponseStyle = 'fields', 53 + ThrowOnError extends boolean = boolean, 54 + Url extends string = string, 55 + > extends Config<{ 56 + responseStyle: TResponseStyle; 57 + throwOnError: ThrowOnError; 58 + }> { 59 + /** 60 + * Any body that you want to add to your request. 61 + * 62 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} 63 + */ 64 + body?: unknown; 65 + path?: Record<string, unknown>; 66 + query?: Record<string, unknown>; 67 + /** 68 + * Security mechanism(s) to use for the request. 69 + */ 70 + security?: ReadonlyArray<Auth>; 71 + url: Url; 72 + } 73 + 74 + export type RequestResult< 75 + TData = unknown, 76 + TError = unknown, 77 + ThrowOnError extends boolean = boolean, 78 + TResponseStyle extends ResponseStyle = 'fields', 79 + > = ThrowOnError extends true 80 + ? Promise< 81 + TResponseStyle extends 'data' 82 + ? TData extends Record<string, unknown> 83 + ? TData[keyof TData] 84 + : TData 85 + : { 86 + data: TData extends Record<string, unknown> 87 + ? TData[keyof TData] 88 + : TData; 89 + request: Request; 90 + response: Response; 91 + } 92 + > 93 + : Promise< 94 + TResponseStyle extends 'data' 95 + ? 96 + | (TData extends Record<string, unknown> 97 + ? TData[keyof TData] 98 + : TData) 99 + | undefined 100 + : ( 101 + | { 102 + data: TData extends Record<string, unknown> 103 + ? TData[keyof TData] 104 + : TData; 105 + error: undefined; 106 + } 107 + | { 108 + data: undefined; 109 + error: TError extends Record<string, unknown> 110 + ? TError[keyof TError] 111 + : TError; 112 + } 113 + ) & { 114 + request: Request; 115 + response: Response; 116 + } 117 + >; 118 + 119 + export interface ClientOptions { 120 + baseUrl?: string; 121 + responseStyle?: ResponseStyle; 122 + throwOnError?: boolean; 123 + } 124 + 125 + type MethodFn = < 126 + TData = unknown, 127 + TError = unknown, 128 + ThrowOnError extends boolean = false, 129 + TResponseStyle extends ResponseStyle = 'fields', 130 + >( 131 + options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'>, 132 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 133 + 134 + type RequestFn = < 135 + TData = unknown, 136 + TError = unknown, 137 + ThrowOnError extends boolean = false, 138 + TResponseStyle extends ResponseStyle = 'fields', 139 + >( 140 + options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'> & 141 + Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, 'method'>, 142 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 143 + 144 + type BuildUrlFn = < 145 + TData extends { 146 + body?: unknown; 147 + path?: Record<string, unknown>; 148 + query?: Record<string, unknown>; 149 + url: string; 150 + }, 151 + >( 152 + options: Pick<TData, 'url'> & Options<TData>, 153 + ) => string; 154 + 155 + export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & { 156 + interceptors: Middleware<Request, Response, unknown, RequestOptions>; 157 + }; 158 + 159 + /** 160 + * The `createClientConfig()` function will be called on client initialization 161 + * and the returned object will become the client's initial configuration. 162 + * 163 + * You may want to initialize your client this way instead of calling 164 + * `setConfig()`. This is useful for example if you're using Next.js 165 + * to ensure your client always has the correct values. 166 + */ 167 + export type CreateClientConfig<T extends ClientOptions = ClientOptions> = ( 168 + override?: Config<ClientOptions & T>, 169 + ) => Config<Required<ClientOptions> & T>; 170 + 171 + export interface TDataShape { 172 + body?: unknown; 173 + headers?: unknown; 174 + path?: unknown; 175 + query?: unknown; 176 + url: string; 177 + } 178 + 179 + type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>; 180 + 181 + export type Options< 182 + TData extends TDataShape = TDataShape, 183 + ThrowOnError extends boolean = boolean, 184 + TResponseStyle extends ResponseStyle = 'fields', 185 + > = OmitKeys< 186 + RequestOptions<TResponseStyle, ThrowOnError>, 187 + 'body' | 'path' | 'query' | 'url' 188 + > & 189 + Omit<TData, 'url'>; 190 + 191 + export type OptionsLegacyParser< 192 + TData = unknown, 193 + ThrowOnError extends boolean = boolean, 194 + TResponseStyle extends ResponseStyle = 'fields', 195 + > = TData extends { body?: any } 196 + ? TData extends { headers?: any } 197 + ? OmitKeys< 198 + RequestOptions<TResponseStyle, ThrowOnError>, 199 + 'body' | 'headers' | 'url' 200 + > & 201 + TData 202 + : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'body' | 'url'> & 203 + TData & 204 + Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'headers'> 205 + : TData extends { headers?: any } 206 + ? OmitKeys< 207 + RequestOptions<TResponseStyle, ThrowOnError>, 208 + 'headers' | 'url' 209 + > & 210 + TData & 211 + Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'body'> 212 + : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'url'> & TData;
+415
examples/openapi-ts-sample/src/client/client/utils.ts
··· 1 + import { getAuthToken } from '../core/auth'; 2 + import type { 3 + QuerySerializer, 4 + QuerySerializerOptions, 5 + } from '../core/bodySerializer'; 6 + import { jsonBodySerializer } from '../core/bodySerializer'; 7 + import { 8 + serializeArrayParam, 9 + serializeObjectParam, 10 + serializePrimitiveParam, 11 + } from '../core/pathSerializer'; 12 + import type { Client, ClientOptions, Config, RequestOptions } from './types'; 13 + 14 + interface PathSerializer { 15 + path: Record<string, unknown>; 16 + url: string; 17 + } 18 + 19 + const PATH_PARAM_RE = /\{[^{}]+\}/g; 20 + 21 + type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; 22 + type MatrixStyle = 'label' | 'matrix' | 'simple'; 23 + type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 24 + 25 + const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { 26 + let url = _url; 27 + const matches = _url.match(PATH_PARAM_RE); 28 + if (matches) { 29 + for (const match of matches) { 30 + let explode = false; 31 + let name = match.substring(1, match.length - 1); 32 + let style: ArraySeparatorStyle = 'simple'; 33 + 34 + if (name.endsWith('*')) { 35 + explode = true; 36 + name = name.substring(0, name.length - 1); 37 + } 38 + 39 + if (name.startsWith('.')) { 40 + name = name.substring(1); 41 + style = 'label'; 42 + } else if (name.startsWith(';')) { 43 + name = name.substring(1); 44 + style = 'matrix'; 45 + } 46 + 47 + const value = path[name]; 48 + 49 + if (value === undefined || value === null) { 50 + continue; 51 + } 52 + 53 + if (Array.isArray(value)) { 54 + url = url.replace( 55 + match, 56 + serializeArrayParam({ explode, name, style, value }), 57 + ); 58 + continue; 59 + } 60 + 61 + if (typeof value === 'object') { 62 + url = url.replace( 63 + match, 64 + serializeObjectParam({ 65 + explode, 66 + name, 67 + style, 68 + value: value as Record<string, unknown>, 69 + valueOnly: true, 70 + }), 71 + ); 72 + continue; 73 + } 74 + 75 + if (style === 'matrix') { 76 + url = url.replace( 77 + match, 78 + `;${serializePrimitiveParam({ 79 + name, 80 + value: value as string, 81 + })}`, 82 + ); 83 + continue; 84 + } 85 + 86 + const replaceValue = encodeURIComponent( 87 + style === 'label' ? `.${value as string}` : (value as string), 88 + ); 89 + url = url.replace(match, replaceValue); 90 + } 91 + } 92 + return url; 93 + }; 94 + 95 + export const createQuerySerializer = <T = unknown>({ 96 + allowReserved, 97 + array, 98 + object, 99 + }: QuerySerializerOptions = {}) => { 100 + const querySerializer = (queryParams: T) => { 101 + const search: string[] = []; 102 + if (queryParams && typeof queryParams === 'object') { 103 + for (const name in queryParams) { 104 + const value = queryParams[name]; 105 + 106 + if (value === undefined || value === null) { 107 + continue; 108 + } 109 + 110 + if (Array.isArray(value)) { 111 + const serializedArray = serializeArrayParam({ 112 + allowReserved, 113 + explode: true, 114 + name, 115 + style: 'form', 116 + value, 117 + ...array, 118 + }); 119 + if (serializedArray) search.push(serializedArray); 120 + } else if (typeof value === 'object') { 121 + const serializedObject = serializeObjectParam({ 122 + allowReserved, 123 + explode: true, 124 + name, 125 + style: 'deepObject', 126 + value: value as Record<string, unknown>, 127 + ...object, 128 + }); 129 + if (serializedObject) search.push(serializedObject); 130 + } else { 131 + const serializedPrimitive = serializePrimitiveParam({ 132 + allowReserved, 133 + name, 134 + value: value as string, 135 + }); 136 + if (serializedPrimitive) search.push(serializedPrimitive); 137 + } 138 + } 139 + } 140 + return search.join('&'); 141 + }; 142 + return querySerializer; 143 + }; 144 + 145 + /** 146 + * Infers parseAs value from provided Content-Type header. 147 + */ 148 + export const getParseAs = ( 149 + contentType: string | null, 150 + ): Exclude<Config['parseAs'], 'auto'> => { 151 + if (!contentType) { 152 + // If no Content-Type header is provided, the best we can do is return the raw response body, 153 + // which is effectively the same as the 'stream' option. 154 + return 'stream'; 155 + } 156 + 157 + const cleanContent = contentType.split(';')[0]?.trim(); 158 + 159 + if (!cleanContent) { 160 + return; 161 + } 162 + 163 + if ( 164 + cleanContent.startsWith('application/json') || 165 + cleanContent.endsWith('+json') 166 + ) { 167 + return 'json'; 168 + } 169 + 170 + if (cleanContent === 'multipart/form-data') { 171 + return 'formData'; 172 + } 173 + 174 + if ( 175 + ['application/', 'audio/', 'image/', 'video/'].some((type) => 176 + cleanContent.startsWith(type), 177 + ) 178 + ) { 179 + return 'blob'; 180 + } 181 + 182 + if (cleanContent.startsWith('text/')) { 183 + return 'text'; 184 + } 185 + }; 186 + 187 + export const setAuthParams = async ({ 188 + security, 189 + ...options 190 + }: Pick<Required<RequestOptions>, 'security'> & 191 + Pick<RequestOptions, 'auth' | 'query'> & { 192 + headers: Headers; 193 + }) => { 194 + for (const auth of security) { 195 + const token = await getAuthToken(auth, options.auth); 196 + 197 + if (!token) { 198 + continue; 199 + } 200 + 201 + const name = auth.name ?? 'Authorization'; 202 + 203 + switch (auth.in) { 204 + case 'query': 205 + if (!options.query) { 206 + options.query = {}; 207 + } 208 + options.query[name] = token; 209 + break; 210 + case 'cookie': 211 + options.headers.append('Cookie', `${name}=${token}`); 212 + break; 213 + case 'header': 214 + default: 215 + options.headers.set(name, token); 216 + break; 217 + } 218 + 219 + return; 220 + } 221 + }; 222 + 223 + export const buildUrl: Client['buildUrl'] = (options) => { 224 + const url = getUrl({ 225 + baseUrl: options.baseUrl as string, 226 + path: options.path, 227 + query: options.query, 228 + querySerializer: 229 + typeof options.querySerializer === 'function' 230 + ? options.querySerializer 231 + : createQuerySerializer(options.querySerializer), 232 + url: options.url, 233 + }); 234 + return url; 235 + }; 236 + 237 + export const getUrl = ({ 238 + baseUrl, 239 + path, 240 + query, 241 + querySerializer, 242 + url: _url, 243 + }: { 244 + baseUrl?: string; 245 + path?: Record<string, unknown>; 246 + query?: Record<string, unknown>; 247 + querySerializer: QuerySerializer; 248 + url: string; 249 + }) => { 250 + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; 251 + let url = (baseUrl ?? '') + pathUrl; 252 + if (path) { 253 + url = defaultPathSerializer({ path, url }); 254 + } 255 + let search = query ? querySerializer(query) : ''; 256 + if (search.startsWith('?')) { 257 + search = search.substring(1); 258 + } 259 + if (search) { 260 + url += `?${search}`; 261 + } 262 + return url; 263 + }; 264 + 265 + export const mergeConfigs = (a: Config, b: Config): Config => { 266 + const config = { ...a, ...b }; 267 + if (config.baseUrl?.endsWith('/')) { 268 + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); 269 + } 270 + config.headers = mergeHeaders(a.headers, b.headers); 271 + return config; 272 + }; 273 + 274 + export const mergeHeaders = ( 275 + ...headers: Array<Required<Config>['headers'] | undefined> 276 + ): Headers => { 277 + const mergedHeaders = new Headers(); 278 + for (const header of headers) { 279 + if (!header || typeof header !== 'object') { 280 + continue; 281 + } 282 + 283 + const iterator = 284 + header instanceof Headers ? header.entries() : Object.entries(header); 285 + 286 + for (const [key, value] of iterator) { 287 + if (value === null) { 288 + mergedHeaders.delete(key); 289 + } else if (Array.isArray(value)) { 290 + for (const v of value) { 291 + mergedHeaders.append(key, v as string); 292 + } 293 + } else if (value !== undefined) { 294 + // assume object headers are meant to be JSON stringified, i.e. their 295 + // content value in OpenAPI specification is 'application/json' 296 + mergedHeaders.set( 297 + key, 298 + typeof value === 'object' ? JSON.stringify(value) : (value as string), 299 + ); 300 + } 301 + } 302 + } 303 + return mergedHeaders; 304 + }; 305 + 306 + type ErrInterceptor<Err, Res, Req, Options> = ( 307 + error: Err, 308 + response: Res, 309 + request: Req, 310 + options: Options, 311 + ) => Err | Promise<Err>; 312 + 313 + type ReqInterceptor<Req, Options> = ( 314 + request: Req, 315 + options: Options, 316 + ) => Req | Promise<Req>; 317 + 318 + type ResInterceptor<Res, Req, Options> = ( 319 + response: Res, 320 + request: Req, 321 + options: Options, 322 + ) => Res | Promise<Res>; 323 + 324 + class Interceptors<Interceptor> { 325 + _fns: (Interceptor | null)[]; 326 + 327 + constructor() { 328 + this._fns = []; 329 + } 330 + 331 + clear() { 332 + this._fns = []; 333 + } 334 + 335 + getInterceptorIndex(id: number | Interceptor): number { 336 + if (typeof id === 'number') { 337 + return this._fns[id] ? id : -1; 338 + } else { 339 + return this._fns.indexOf(id); 340 + } 341 + } 342 + exists(id: number | Interceptor) { 343 + const index = this.getInterceptorIndex(id); 344 + return !!this._fns[index]; 345 + } 346 + 347 + eject(id: number | Interceptor) { 348 + const index = this.getInterceptorIndex(id); 349 + if (this._fns[index]) { 350 + this._fns[index] = null; 351 + } 352 + } 353 + 354 + update(id: number | Interceptor, fn: Interceptor) { 355 + const index = this.getInterceptorIndex(id); 356 + if (this._fns[index]) { 357 + this._fns[index] = fn; 358 + return id; 359 + } else { 360 + return false; 361 + } 362 + } 363 + 364 + use(fn: Interceptor) { 365 + this._fns = [...this._fns, fn]; 366 + return this._fns.length - 1; 367 + } 368 + } 369 + 370 + // `createInterceptors()` response, meant for external use as it does not 371 + // expose internals 372 + export interface Middleware<Req, Res, Err, Options> { 373 + error: Pick< 374 + Interceptors<ErrInterceptor<Err, Res, Req, Options>>, 375 + 'eject' | 'use' 376 + >; 377 + request: Pick<Interceptors<ReqInterceptor<Req, Options>>, 'eject' | 'use'>; 378 + response: Pick< 379 + Interceptors<ResInterceptor<Res, Req, Options>>, 380 + 'eject' | 'use' 381 + >; 382 + } 383 + 384 + // do not add `Middleware` as return type so we can use _fns internally 385 + export const createInterceptors = <Req, Res, Err, Options>() => ({ 386 + error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(), 387 + request: new Interceptors<ReqInterceptor<Req, Options>>(), 388 + response: new Interceptors<ResInterceptor<Res, Req, Options>>(), 389 + }); 390 + 391 + const defaultQuerySerializer = createQuerySerializer({ 392 + allowReserved: false, 393 + array: { 394 + explode: true, 395 + style: 'form', 396 + }, 397 + object: { 398 + explode: true, 399 + style: 'deepObject', 400 + }, 401 + }); 402 + 403 + const defaultHeaders = { 404 + 'Content-Type': 'application/json', 405 + }; 406 + 407 + export const createConfig = <T extends ClientOptions = ClientOptions>( 408 + override: Config<Omit<ClientOptions, keyof T> & T> = {}, 409 + ): Config<Omit<ClientOptions, keyof T> & T> => ({ 410 + ...jsonBodySerializer, 411 + headers: defaultHeaders, 412 + parseAs: 'auto', 413 + querySerializer: defaultQuerySerializer, 414 + ...override, 415 + });
+40
examples/openapi-ts-sample/src/client/core/auth.ts
··· 1 + export type AuthToken = string | undefined; 2 + 3 + export interface Auth { 4 + /** 5 + * Which part of the request do we use to send the auth? 6 + * 7 + * @default 'header' 8 + */ 9 + in?: 'header' | 'query' | 'cookie'; 10 + /** 11 + * Header or query parameter name. 12 + * 13 + * @default 'Authorization' 14 + */ 15 + name?: string; 16 + scheme?: 'basic' | 'bearer'; 17 + type: 'apiKey' | 'http'; 18 + } 19 + 20 + export const getAuthToken = async ( 21 + auth: Auth, 22 + callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken, 23 + ): Promise<string | undefined> => { 24 + const token = 25 + typeof callback === 'function' ? await callback(auth) : callback; 26 + 27 + if (!token) { 28 + return; 29 + } 30 + 31 + if (auth.scheme === 'bearer') { 32 + return `Bearer ${token}`; 33 + } 34 + 35 + if (auth.scheme === 'basic') { 36 + return `Basic ${btoa(token)}`; 37 + } 38 + 39 + return token; 40 + };
+84
examples/openapi-ts-sample/src/client/core/bodySerializer.ts
··· 1 + import type { 2 + ArrayStyle, 3 + ObjectStyle, 4 + SerializerOptions, 5 + } from './pathSerializer'; 6 + 7 + export type QuerySerializer = (query: Record<string, unknown>) => string; 8 + 9 + export type BodySerializer = (body: any) => any; 10 + 11 + export interface QuerySerializerOptions { 12 + allowReserved?: boolean; 13 + array?: SerializerOptions<ArrayStyle>; 14 + object?: SerializerOptions<ObjectStyle>; 15 + } 16 + 17 + const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { 18 + if (typeof value === 'string' || value instanceof Blob) { 19 + data.append(key, value); 20 + } else { 21 + data.append(key, JSON.stringify(value)); 22 + } 23 + }; 24 + 25 + const serializeUrlSearchParamsPair = ( 26 + data: URLSearchParams, 27 + key: string, 28 + value: unknown, 29 + ) => { 30 + if (typeof value === 'string') { 31 + data.append(key, value); 32 + } else { 33 + data.append(key, JSON.stringify(value)); 34 + } 35 + }; 36 + 37 + export const formDataBodySerializer = { 38 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 39 + body: T, 40 + ) => { 41 + const data = new FormData(); 42 + 43 + Object.entries(body).forEach(([key, value]) => { 44 + if (value === undefined || value === null) { 45 + return; 46 + } 47 + if (Array.isArray(value)) { 48 + value.forEach((v) => serializeFormDataPair(data, key, v)); 49 + } else { 50 + serializeFormDataPair(data, key, value); 51 + } 52 + }); 53 + 54 + return data; 55 + }, 56 + }; 57 + 58 + export const jsonBodySerializer = { 59 + bodySerializer: <T>(body: T) => 60 + JSON.stringify(body, (key, value) => 61 + typeof value === 'bigint' ? value.toString() : value, 62 + ), 63 + }; 64 + 65 + export const urlSearchParamsBodySerializer = { 66 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 67 + body: T, 68 + ) => { 69 + const data = new URLSearchParams(); 70 + 71 + Object.entries(body).forEach(([key, value]) => { 72 + if (value === undefined || value === null) { 73 + return; 74 + } 75 + if (Array.isArray(value)) { 76 + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); 77 + } else { 78 + serializeUrlSearchParamsPair(data, key, value); 79 + } 80 + }); 81 + 82 + return data.toString(); 83 + }, 84 + };
+141
examples/openapi-ts-sample/src/client/core/params.ts
··· 1 + type Slot = 'body' | 'headers' | 'path' | 'query'; 2 + 3 + export type Field = 4 + | { 5 + in: Exclude<Slot, 'body'>; 6 + key: string; 7 + map?: string; 8 + } 9 + | { 10 + in: Extract<Slot, 'body'>; 11 + key?: string; 12 + map?: string; 13 + }; 14 + 15 + export interface Fields { 16 + allowExtra?: Partial<Record<Slot, boolean>>; 17 + args?: ReadonlyArray<Field>; 18 + } 19 + 20 + export type FieldsConfig = ReadonlyArray<Field | Fields>; 21 + 22 + const extraPrefixesMap: Record<string, Slot> = { 23 + $body_: 'body', 24 + $headers_: 'headers', 25 + $path_: 'path', 26 + $query_: 'query', 27 + }; 28 + const extraPrefixes = Object.entries(extraPrefixesMap); 29 + 30 + type KeyMap = Map< 31 + string, 32 + { 33 + in: Slot; 34 + map?: string; 35 + } 36 + >; 37 + 38 + const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { 39 + if (!map) { 40 + map = new Map(); 41 + } 42 + 43 + for (const config of fields) { 44 + if ('in' in config) { 45 + if (config.key) { 46 + map.set(config.key, { 47 + in: config.in, 48 + map: config.map, 49 + }); 50 + } 51 + } else if (config.args) { 52 + buildKeyMap(config.args, map); 53 + } 54 + } 55 + 56 + return map; 57 + }; 58 + 59 + interface Params { 60 + body: unknown; 61 + headers: Record<string, unknown>; 62 + path: Record<string, unknown>; 63 + query: Record<string, unknown>; 64 + } 65 + 66 + const stripEmptySlots = (params: Params) => { 67 + for (const [slot, value] of Object.entries(params)) { 68 + if (value && typeof value === 'object' && !Object.keys(value).length) { 69 + delete params[slot as Slot]; 70 + } 71 + } 72 + }; 73 + 74 + export const buildClientParams = ( 75 + args: ReadonlyArray<unknown>, 76 + fields: FieldsConfig, 77 + ) => { 78 + const params: Params = { 79 + body: {}, 80 + headers: {}, 81 + path: {}, 82 + query: {}, 83 + }; 84 + 85 + const map = buildKeyMap(fields); 86 + 87 + let config: FieldsConfig[number] | undefined; 88 + 89 + for (const [index, arg] of args.entries()) { 90 + if (fields[index]) { 91 + config = fields[index]; 92 + } 93 + 94 + if (!config) { 95 + continue; 96 + } 97 + 98 + if ('in' in config) { 99 + if (config.key) { 100 + const field = map.get(config.key)!; 101 + const name = field.map || config.key; 102 + (params[field.in] as Record<string, unknown>)[name] = arg; 103 + } else { 104 + params.body = arg; 105 + } 106 + } else { 107 + for (const [key, value] of Object.entries(arg ?? {})) { 108 + const field = map.get(key); 109 + 110 + if (field) { 111 + const name = field.map || key; 112 + (params[field.in] as Record<string, unknown>)[name] = value; 113 + } else { 114 + const extra = extraPrefixes.find(([prefix]) => 115 + key.startsWith(prefix), 116 + ); 117 + 118 + if (extra) { 119 + const [prefix, slot] = extra; 120 + (params[slot] as Record<string, unknown>)[ 121 + key.slice(prefix.length) 122 + ] = value; 123 + } else { 124 + for (const [slot, allowed] of Object.entries( 125 + config.allowExtra ?? {}, 126 + )) { 127 + if (allowed) { 128 + (params[slot as Slot] as Record<string, unknown>)[key] = value; 129 + break; 130 + } 131 + } 132 + } 133 + } 134 + } 135 + } 136 + } 137 + 138 + stripEmptySlots(params); 139 + 140 + return params; 141 + };
+179
examples/openapi-ts-sample/src/client/core/pathSerializer.ts
··· 1 + interface SerializeOptions<T> 2 + extends SerializePrimitiveOptions, 3 + SerializerOptions<T> {} 4 + 5 + interface SerializePrimitiveOptions { 6 + allowReserved?: boolean; 7 + name: string; 8 + } 9 + 10 + export interface SerializerOptions<T> { 11 + /** 12 + * @default true 13 + */ 14 + explode: boolean; 15 + style: T; 16 + } 17 + 18 + export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; 19 + export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 20 + type MatrixStyle = 'label' | 'matrix' | 'simple'; 21 + export type ObjectStyle = 'form' | 'deepObject'; 22 + type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; 23 + 24 + interface SerializePrimitiveParam extends SerializePrimitiveOptions { 25 + value: string; 26 + } 27 + 28 + export const separatorArrayExplode = (style: ArraySeparatorStyle) => { 29 + switch (style) { 30 + case 'label': 31 + return '.'; 32 + case 'matrix': 33 + return ';'; 34 + case 'simple': 35 + return ','; 36 + default: 37 + return '&'; 38 + } 39 + }; 40 + 41 + export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { 42 + switch (style) { 43 + case 'form': 44 + return ','; 45 + case 'pipeDelimited': 46 + return '|'; 47 + case 'spaceDelimited': 48 + return '%20'; 49 + default: 50 + return ','; 51 + } 52 + }; 53 + 54 + export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { 55 + switch (style) { 56 + case 'label': 57 + return '.'; 58 + case 'matrix': 59 + return ';'; 60 + case 'simple': 61 + return ','; 62 + default: 63 + return '&'; 64 + } 65 + }; 66 + 67 + export const serializeArrayParam = ({ 68 + allowReserved, 69 + explode, 70 + name, 71 + style, 72 + value, 73 + }: SerializeOptions<ArraySeparatorStyle> & { 74 + value: unknown[]; 75 + }) => { 76 + if (!explode) { 77 + const joinedValues = ( 78 + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) 79 + ).join(separatorArrayNoExplode(style)); 80 + switch (style) { 81 + case 'label': 82 + return `.${joinedValues}`; 83 + case 'matrix': 84 + return `;${name}=${joinedValues}`; 85 + case 'simple': 86 + return joinedValues; 87 + default: 88 + return `${name}=${joinedValues}`; 89 + } 90 + } 91 + 92 + const separator = separatorArrayExplode(style); 93 + const joinedValues = value 94 + .map((v) => { 95 + if (style === 'label' || style === 'simple') { 96 + return allowReserved ? v : encodeURIComponent(v as string); 97 + } 98 + 99 + return serializePrimitiveParam({ 100 + allowReserved, 101 + name, 102 + value: v as string, 103 + }); 104 + }) 105 + .join(separator); 106 + return style === 'label' || style === 'matrix' 107 + ? separator + joinedValues 108 + : joinedValues; 109 + }; 110 + 111 + export const serializePrimitiveParam = ({ 112 + allowReserved, 113 + name, 114 + value, 115 + }: SerializePrimitiveParam) => { 116 + if (value === undefined || value === null) { 117 + return ''; 118 + } 119 + 120 + if (typeof value === 'object') { 121 + throw new Error( 122 + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', 123 + ); 124 + } 125 + 126 + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; 127 + }; 128 + 129 + export const serializeObjectParam = ({ 130 + allowReserved, 131 + explode, 132 + name, 133 + style, 134 + value, 135 + valueOnly, 136 + }: SerializeOptions<ObjectSeparatorStyle> & { 137 + value: Record<string, unknown> | Date; 138 + valueOnly?: boolean; 139 + }) => { 140 + if (value instanceof Date) { 141 + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; 142 + } 143 + 144 + if (style !== 'deepObject' && !explode) { 145 + let values: string[] = []; 146 + Object.entries(value).forEach(([key, v]) => { 147 + values = [ 148 + ...values, 149 + key, 150 + allowReserved ? (v as string) : encodeURIComponent(v as string), 151 + ]; 152 + }); 153 + const joinedValues = values.join(','); 154 + switch (style) { 155 + case 'form': 156 + return `${name}=${joinedValues}`; 157 + case 'label': 158 + return `.${joinedValues}`; 159 + case 'matrix': 160 + return `;${name}=${joinedValues}`; 161 + default: 162 + return joinedValues; 163 + } 164 + } 165 + 166 + const separator = separatorObjectExplode(style); 167 + const joinedValues = Object.entries(value) 168 + .map(([key, v]) => 169 + serializePrimitiveParam({ 170 + allowReserved, 171 + name: style === 'deepObject' ? `${name}[${key}]` : key, 172 + value: v as string, 173 + }), 174 + ) 175 + .join(separator); 176 + return style === 'label' || style === 'matrix' 177 + ? separator + joinedValues 178 + : joinedValues; 179 + };
+98
examples/openapi-ts-sample/src/client/core/types.ts
··· 1 + import type { Auth, AuthToken } from './auth'; 2 + import type { 3 + BodySerializer, 4 + QuerySerializer, 5 + QuerySerializerOptions, 6 + } from './bodySerializer'; 7 + 8 + export interface Client< 9 + RequestFn = never, 10 + Config = unknown, 11 + MethodFn = never, 12 + BuildUrlFn = never, 13 + > { 14 + /** 15 + * Returns the final request URL. 16 + */ 17 + buildUrl: BuildUrlFn; 18 + connect: MethodFn; 19 + delete: MethodFn; 20 + get: MethodFn; 21 + getConfig: () => Config; 22 + head: MethodFn; 23 + options: MethodFn; 24 + patch: MethodFn; 25 + post: MethodFn; 26 + put: MethodFn; 27 + request: RequestFn; 28 + setConfig: (config: Config) => Config; 29 + trace: MethodFn; 30 + } 31 + 32 + export interface Config { 33 + /** 34 + * Auth token or a function returning auth token. The resolved value will be 35 + * added to the request payload as defined by its `security` array. 36 + */ 37 + auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken; 38 + /** 39 + * A function for serializing request body parameter. By default, 40 + * {@link JSON.stringify()} will be used. 41 + */ 42 + bodySerializer?: BodySerializer | null; 43 + /** 44 + * An object containing any HTTP headers that you want to pre-populate your 45 + * `Headers` object with. 46 + * 47 + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} 48 + */ 49 + headers?: 50 + | RequestInit['headers'] 51 + | Record< 52 + string, 53 + | string 54 + | number 55 + | boolean 56 + | (string | number | boolean)[] 57 + | null 58 + | undefined 59 + | unknown 60 + >; 61 + /** 62 + * The request method. 63 + * 64 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} 65 + */ 66 + method?: 67 + | 'CONNECT' 68 + | 'DELETE' 69 + | 'GET' 70 + | 'HEAD' 71 + | 'OPTIONS' 72 + | 'PATCH' 73 + | 'POST' 74 + | 'PUT' 75 + | 'TRACE'; 76 + /** 77 + * A function for serializing request query parameters. By default, arrays 78 + * will be exploded in form style, objects will be exploded in deepObject 79 + * style, and reserved characters are percent-encoded. 80 + * 81 + * This method will have no effect if the native `paramsSerializer()` Axios 82 + * API function is used. 83 + * 84 + * {@link https://swagger.io/docs/specification/serialization/#query View examples} 85 + */ 86 + querySerializer?: QuerySerializer | QuerySerializerOptions; 87 + /** 88 + * A function transforming response data before it's returned. This is useful 89 + * for post-processing data, e.g. converting ISO strings into Date objects. 90 + */ 91 + responseTransformer?: (data: unknown) => Promise<unknown>; 92 + /** 93 + * A function validating response data. This is useful if you want to ensure 94 + * the response conforms to the desired shape, so it can be safely passed to 95 + * the transformers and returned to the user. 96 + */ 97 + responseValidator?: (data: unknown) => Promise<unknown>; 98 + }
+5 -10
examples/openapi-ts-sample/src/client/sdk.gen.ts
··· 1 1 // This file is auto-generated by @hey-api/openapi-ts 2 2 3 - import type { 4 - Client, 5 - Options as ClientOptions, 6 - TDataShape, 7 - } from '@hey-api/client-fetch'; 8 - 3 + import type { Client, Options as ClientOptions, TDataShape } from './client'; 9 4 import { client as _heyApiClient } from './client.gen'; 10 5 import type { 11 6 AddPetData, ··· 106 101 ...options, 107 102 headers: { 108 103 'Content-Type': 'application/json', 109 - ...options?.headers, 104 + ...options.headers, 110 105 }, 111 106 }); 112 107 ··· 132 127 ...options, 133 128 headers: { 134 129 'Content-Type': 'application/json', 135 - ...options?.headers, 130 + ...options.headers, 136 131 }, 137 132 }); 138 133 ··· 273 268 ...options, 274 269 headers: { 275 270 'Content-Type': 'application/octet-stream', 276 - ...options?.headers, 271 + ...options.headers, 277 272 }, 278 273 }); 279 274 ··· 471 466 ...options, 472 467 headers: { 473 468 'Content-Type': 'application/json', 474 - ...options?.headers, 469 + ...options.headers, 475 470 }, 476 471 });
+1 -1
examples/openapi-ts-sample/src/hey-api.ts
··· 1 - import type { CreateClientConfig } from '@hey-api/client-fetch'; 1 + import type { CreateClientConfig } from './client/client'; 2 2 3 3 export const createClientConfig: CreateClientConfig = (config) => ({ 4 4 ...config,
-1
examples/openapi-ts-tanstack-angular-query-experimental/package.json
··· 21 21 "@angular/platform-browser": "^19.2.0", 22 22 "@angular/platform-browser-dynamic": "^19.2.0", 23 23 "@angular/router": "^19.2.0", 24 - "@hey-api/client-fetch": "workspace:*", 25 24 "@tanstack/angular-query-experimental": "5.73.3", 26 25 "rxjs": "~7.8.0", 27 26 "tslib": "^2.8.1",
+1 -2
examples/openapi-ts-tanstack-angular-query-experimental/src/client/client.gen.ts
··· 5 5 type Config, 6 6 createClient, 7 7 createConfig, 8 - } from '@hey-api/client-fetch'; 9 - 8 + } from './client'; 10 9 import type { ClientOptions } from './types.gen'; 11 10 12 11 /**
+181
examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/client.ts
··· 1 + import type { Client, Config, RequestOptions } from './types'; 2 + import { 3 + buildUrl, 4 + createConfig, 5 + createInterceptors, 6 + getParseAs, 7 + mergeConfigs, 8 + mergeHeaders, 9 + setAuthParams, 10 + } from './utils'; 11 + 12 + type ReqInit = Omit<RequestInit, 'body' | 'headers'> & { 13 + body?: any; 14 + headers: ReturnType<typeof mergeHeaders>; 15 + }; 16 + 17 + export const createClient = (config: Config = {}): Client => { 18 + let _config = mergeConfigs(createConfig(), config); 19 + 20 + const getConfig = (): Config => ({ ..._config }); 21 + 22 + const setConfig = (config: Config): Config => { 23 + _config = mergeConfigs(_config, config); 24 + return getConfig(); 25 + }; 26 + 27 + const interceptors = createInterceptors< 28 + Request, 29 + Response, 30 + unknown, 31 + RequestOptions 32 + >(); 33 + 34 + const request: Client['request'] = async (options) => { 35 + const opts = { 36 + ..._config, 37 + ...options, 38 + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, 39 + headers: mergeHeaders(_config.headers, options.headers), 40 + }; 41 + 42 + if (opts.security) { 43 + await setAuthParams({ 44 + ...opts, 45 + security: opts.security, 46 + }); 47 + } 48 + 49 + if (opts.body && opts.bodySerializer) { 50 + opts.body = opts.bodySerializer(opts.body); 51 + } 52 + 53 + // remove Content-Type header if body is empty to avoid sending invalid requests 54 + if (opts.body === undefined || opts.body === '') { 55 + opts.headers.delete('Content-Type'); 56 + } 57 + 58 + const url = buildUrl(opts); 59 + const requestInit: ReqInit = { 60 + redirect: 'follow', 61 + ...opts, 62 + }; 63 + 64 + let request = new Request(url, requestInit); 65 + 66 + for (const fn of interceptors.request._fns) { 67 + if (fn) { 68 + request = await fn(request, opts); 69 + } 70 + } 71 + 72 + // fetch must be assigned here, otherwise it would throw the error: 73 + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation 74 + const _fetch = opts.fetch!; 75 + let response = await _fetch(request); 76 + 77 + for (const fn of interceptors.response._fns) { 78 + if (fn) { 79 + response = await fn(response, request, opts); 80 + } 81 + } 82 + 83 + const result = { 84 + request, 85 + response, 86 + }; 87 + 88 + if (response.ok) { 89 + if ( 90 + response.status === 204 || 91 + response.headers.get('Content-Length') === '0' 92 + ) { 93 + return opts.responseStyle === 'data' 94 + ? {} 95 + : { 96 + data: {}, 97 + ...result, 98 + }; 99 + } 100 + 101 + const parseAs = 102 + (opts.parseAs === 'auto' 103 + ? getParseAs(response.headers.get('Content-Type')) 104 + : opts.parseAs) ?? 'json'; 105 + 106 + if (parseAs === 'stream') { 107 + return opts.responseStyle === 'data' 108 + ? response.body 109 + : { 110 + data: response.body, 111 + ...result, 112 + }; 113 + } 114 + 115 + let data = await response[parseAs](); 116 + if (parseAs === 'json') { 117 + if (opts.responseValidator) { 118 + await opts.responseValidator(data); 119 + } 120 + 121 + if (opts.responseTransformer) { 122 + data = await opts.responseTransformer(data); 123 + } 124 + } 125 + 126 + return opts.responseStyle === 'data' 127 + ? data 128 + : { 129 + data, 130 + ...result, 131 + }; 132 + } 133 + 134 + let error = await response.text(); 135 + 136 + try { 137 + error = JSON.parse(error); 138 + } catch { 139 + // noop 140 + } 141 + 142 + let finalError = error; 143 + 144 + for (const fn of interceptors.error._fns) { 145 + if (fn) { 146 + finalError = (await fn(error, response, request, opts)) as string; 147 + } 148 + } 149 + 150 + finalError = finalError || ({} as string); 151 + 152 + if (opts.throwOnError) { 153 + throw finalError; 154 + } 155 + 156 + // TODO: we probably want to return error and improve types 157 + return opts.responseStyle === 'data' 158 + ? undefined 159 + : { 160 + error: finalError, 161 + ...result, 162 + }; 163 + }; 164 + 165 + return { 166 + buildUrl, 167 + connect: (options) => request({ ...options, method: 'CONNECT' }), 168 + delete: (options) => request({ ...options, method: 'DELETE' }), 169 + get: (options) => request({ ...options, method: 'GET' }), 170 + getConfig, 171 + head: (options) => request({ ...options, method: 'HEAD' }), 172 + interceptors, 173 + options: (options) => request({ ...options, method: 'OPTIONS' }), 174 + patch: (options) => request({ ...options, method: 'PATCH' }), 175 + post: (options) => request({ ...options, method: 'POST' }), 176 + put: (options) => request({ ...options, method: 'PUT' }), 177 + request, 178 + setConfig, 179 + trace: (options) => request({ ...options, method: 'TRACE' }), 180 + }; 181 + };
+22
examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/index.ts
··· 1 + export type { Auth } from '../core/auth'; 2 + export type { QuerySerializerOptions } from '../core/bodySerializer'; 3 + export { 4 + formDataBodySerializer, 5 + jsonBodySerializer, 6 + urlSearchParamsBodySerializer, 7 + } from '../core/bodySerializer'; 8 + export { buildClientParams } from '../core/params'; 9 + export { createClient } from './client'; 10 + export type { 11 + Client, 12 + ClientOptions, 13 + Config, 14 + CreateClientConfig, 15 + Options, 16 + OptionsLegacyParser, 17 + RequestOptions, 18 + RequestResult, 19 + ResponseStyle, 20 + TDataShape, 21 + } from './types'; 22 + export { createConfig, mergeHeaders } from './utils';
+212
examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/types.ts
··· 1 + import type { Auth } from '../core/auth'; 2 + import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; 3 + import type { Middleware } from './utils'; 4 + 5 + export type ResponseStyle = 'data' | 'fields'; 6 + 7 + export interface Config<T extends ClientOptions = ClientOptions> 8 + extends Omit<RequestInit, 'body' | 'headers' | 'method'>, 9 + CoreConfig { 10 + /** 11 + * Base URL for all requests made by this client. 12 + */ 13 + baseUrl?: T['baseUrl']; 14 + /** 15 + * Fetch API implementation. You can use this option to provide a custom 16 + * fetch instance. 17 + * 18 + * @default globalThis.fetch 19 + */ 20 + fetch?: (request: Request) => ReturnType<typeof fetch>; 21 + /** 22 + * Please don't use the Fetch client for Next.js applications. The `next` 23 + * options won't have any effect. 24 + * 25 + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. 26 + */ 27 + next?: never; 28 + /** 29 + * Return the response data parsed in a specified format. By default, `auto` 30 + * will infer the appropriate method from the `Content-Type` response header. 31 + * You can override this behavior with any of the {@link Body} methods. 32 + * Select `stream` if you don't want to parse response data at all. 33 + * 34 + * @default 'auto' 35 + */ 36 + parseAs?: Exclude<keyof Body, 'body' | 'bodyUsed'> | 'auto' | 'stream'; 37 + /** 38 + * Should we return only data or multiple fields (data, error, response, etc.)? 39 + * 40 + * @default 'fields' 41 + */ 42 + responseStyle?: ResponseStyle; 43 + /** 44 + * Throw an error instead of returning it in the response? 45 + * 46 + * @default false 47 + */ 48 + throwOnError?: T['throwOnError']; 49 + } 50 + 51 + export interface RequestOptions< 52 + TResponseStyle extends ResponseStyle = 'fields', 53 + ThrowOnError extends boolean = boolean, 54 + Url extends string = string, 55 + > extends Config<{ 56 + responseStyle: TResponseStyle; 57 + throwOnError: ThrowOnError; 58 + }> { 59 + /** 60 + * Any body that you want to add to your request. 61 + * 62 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} 63 + */ 64 + body?: unknown; 65 + path?: Record<string, unknown>; 66 + query?: Record<string, unknown>; 67 + /** 68 + * Security mechanism(s) to use for the request. 69 + */ 70 + security?: ReadonlyArray<Auth>; 71 + url: Url; 72 + } 73 + 74 + export type RequestResult< 75 + TData = unknown, 76 + TError = unknown, 77 + ThrowOnError extends boolean = boolean, 78 + TResponseStyle extends ResponseStyle = 'fields', 79 + > = ThrowOnError extends true 80 + ? Promise< 81 + TResponseStyle extends 'data' 82 + ? TData extends Record<string, unknown> 83 + ? TData[keyof TData] 84 + : TData 85 + : { 86 + data: TData extends Record<string, unknown> 87 + ? TData[keyof TData] 88 + : TData; 89 + request: Request; 90 + response: Response; 91 + } 92 + > 93 + : Promise< 94 + TResponseStyle extends 'data' 95 + ? 96 + | (TData extends Record<string, unknown> 97 + ? TData[keyof TData] 98 + : TData) 99 + | undefined 100 + : ( 101 + | { 102 + data: TData extends Record<string, unknown> 103 + ? TData[keyof TData] 104 + : TData; 105 + error: undefined; 106 + } 107 + | { 108 + data: undefined; 109 + error: TError extends Record<string, unknown> 110 + ? TError[keyof TError] 111 + : TError; 112 + } 113 + ) & { 114 + request: Request; 115 + response: Response; 116 + } 117 + >; 118 + 119 + export interface ClientOptions { 120 + baseUrl?: string; 121 + responseStyle?: ResponseStyle; 122 + throwOnError?: boolean; 123 + } 124 + 125 + type MethodFn = < 126 + TData = unknown, 127 + TError = unknown, 128 + ThrowOnError extends boolean = false, 129 + TResponseStyle extends ResponseStyle = 'fields', 130 + >( 131 + options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'>, 132 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 133 + 134 + type RequestFn = < 135 + TData = unknown, 136 + TError = unknown, 137 + ThrowOnError extends boolean = false, 138 + TResponseStyle extends ResponseStyle = 'fields', 139 + >( 140 + options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'> & 141 + Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, 'method'>, 142 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 143 + 144 + type BuildUrlFn = < 145 + TData extends { 146 + body?: unknown; 147 + path?: Record<string, unknown>; 148 + query?: Record<string, unknown>; 149 + url: string; 150 + }, 151 + >( 152 + options: Pick<TData, 'url'> & Options<TData>, 153 + ) => string; 154 + 155 + export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & { 156 + interceptors: Middleware<Request, Response, unknown, RequestOptions>; 157 + }; 158 + 159 + /** 160 + * The `createClientConfig()` function will be called on client initialization 161 + * and the returned object will become the client's initial configuration. 162 + * 163 + * You may want to initialize your client this way instead of calling 164 + * `setConfig()`. This is useful for example if you're using Next.js 165 + * to ensure your client always has the correct values. 166 + */ 167 + export type CreateClientConfig<T extends ClientOptions = ClientOptions> = ( 168 + override?: Config<ClientOptions & T>, 169 + ) => Config<Required<ClientOptions> & T>; 170 + 171 + export interface TDataShape { 172 + body?: unknown; 173 + headers?: unknown; 174 + path?: unknown; 175 + query?: unknown; 176 + url: string; 177 + } 178 + 179 + type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>; 180 + 181 + export type Options< 182 + TData extends TDataShape = TDataShape, 183 + ThrowOnError extends boolean = boolean, 184 + TResponseStyle extends ResponseStyle = 'fields', 185 + > = OmitKeys< 186 + RequestOptions<TResponseStyle, ThrowOnError>, 187 + 'body' | 'path' | 'query' | 'url' 188 + > & 189 + Omit<TData, 'url'>; 190 + 191 + export type OptionsLegacyParser< 192 + TData = unknown, 193 + ThrowOnError extends boolean = boolean, 194 + TResponseStyle extends ResponseStyle = 'fields', 195 + > = TData extends { body?: any } 196 + ? TData extends { headers?: any } 197 + ? OmitKeys< 198 + RequestOptions<TResponseStyle, ThrowOnError>, 199 + 'body' | 'headers' | 'url' 200 + > & 201 + TData 202 + : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'body' | 'url'> & 203 + TData & 204 + Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'headers'> 205 + : TData extends { headers?: any } 206 + ? OmitKeys< 207 + RequestOptions<TResponseStyle, ThrowOnError>, 208 + 'headers' | 'url' 209 + > & 210 + TData & 211 + Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'body'> 212 + : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'url'> & TData;
+415
examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/utils.ts
··· 1 + import { getAuthToken } from '../core/auth'; 2 + import type { 3 + QuerySerializer, 4 + QuerySerializerOptions, 5 + } from '../core/bodySerializer'; 6 + import { jsonBodySerializer } from '../core/bodySerializer'; 7 + import { 8 + serializeArrayParam, 9 + serializeObjectParam, 10 + serializePrimitiveParam, 11 + } from '../core/pathSerializer'; 12 + import type { Client, ClientOptions, Config, RequestOptions } from './types'; 13 + 14 + interface PathSerializer { 15 + path: Record<string, unknown>; 16 + url: string; 17 + } 18 + 19 + const PATH_PARAM_RE = /\{[^{}]+\}/g; 20 + 21 + type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; 22 + type MatrixStyle = 'label' | 'matrix' | 'simple'; 23 + type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 24 + 25 + const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { 26 + let url = _url; 27 + const matches = _url.match(PATH_PARAM_RE); 28 + if (matches) { 29 + for (const match of matches) { 30 + let explode = false; 31 + let name = match.substring(1, match.length - 1); 32 + let style: ArraySeparatorStyle = 'simple'; 33 + 34 + if (name.endsWith('*')) { 35 + explode = true; 36 + name = name.substring(0, name.length - 1); 37 + } 38 + 39 + if (name.startsWith('.')) { 40 + name = name.substring(1); 41 + style = 'label'; 42 + } else if (name.startsWith(';')) { 43 + name = name.substring(1); 44 + style = 'matrix'; 45 + } 46 + 47 + const value = path[name]; 48 + 49 + if (value === undefined || value === null) { 50 + continue; 51 + } 52 + 53 + if (Array.isArray(value)) { 54 + url = url.replace( 55 + match, 56 + serializeArrayParam({ explode, name, style, value }), 57 + ); 58 + continue; 59 + } 60 + 61 + if (typeof value === 'object') { 62 + url = url.replace( 63 + match, 64 + serializeObjectParam({ 65 + explode, 66 + name, 67 + style, 68 + value: value as Record<string, unknown>, 69 + valueOnly: true, 70 + }), 71 + ); 72 + continue; 73 + } 74 + 75 + if (style === 'matrix') { 76 + url = url.replace( 77 + match, 78 + `;${serializePrimitiveParam({ 79 + name, 80 + value: value as string, 81 + })}`, 82 + ); 83 + continue; 84 + } 85 + 86 + const replaceValue = encodeURIComponent( 87 + style === 'label' ? `.${value as string}` : (value as string), 88 + ); 89 + url = url.replace(match, replaceValue); 90 + } 91 + } 92 + return url; 93 + }; 94 + 95 + export const createQuerySerializer = <T = unknown>({ 96 + allowReserved, 97 + array, 98 + object, 99 + }: QuerySerializerOptions = {}) => { 100 + const querySerializer = (queryParams: T) => { 101 + const search: string[] = []; 102 + if (queryParams && typeof queryParams === 'object') { 103 + for (const name in queryParams) { 104 + const value = queryParams[name]; 105 + 106 + if (value === undefined || value === null) { 107 + continue; 108 + } 109 + 110 + if (Array.isArray(value)) { 111 + const serializedArray = serializeArrayParam({ 112 + allowReserved, 113 + explode: true, 114 + name, 115 + style: 'form', 116 + value, 117 + ...array, 118 + }); 119 + if (serializedArray) search.push(serializedArray); 120 + } else if (typeof value === 'object') { 121 + const serializedObject = serializeObjectParam({ 122 + allowReserved, 123 + explode: true, 124 + name, 125 + style: 'deepObject', 126 + value: value as Record<string, unknown>, 127 + ...object, 128 + }); 129 + if (serializedObject) search.push(serializedObject); 130 + } else { 131 + const serializedPrimitive = serializePrimitiveParam({ 132 + allowReserved, 133 + name, 134 + value: value as string, 135 + }); 136 + if (serializedPrimitive) search.push(serializedPrimitive); 137 + } 138 + } 139 + } 140 + return search.join('&'); 141 + }; 142 + return querySerializer; 143 + }; 144 + 145 + /** 146 + * Infers parseAs value from provided Content-Type header. 147 + */ 148 + export const getParseAs = ( 149 + contentType: string | null, 150 + ): Exclude<Config['parseAs'], 'auto'> => { 151 + if (!contentType) { 152 + // If no Content-Type header is provided, the best we can do is return the raw response body, 153 + // which is effectively the same as the 'stream' option. 154 + return 'stream'; 155 + } 156 + 157 + const cleanContent = contentType.split(';')[0]?.trim(); 158 + 159 + if (!cleanContent) { 160 + return; 161 + } 162 + 163 + if ( 164 + cleanContent.startsWith('application/json') || 165 + cleanContent.endsWith('+json') 166 + ) { 167 + return 'json'; 168 + } 169 + 170 + if (cleanContent === 'multipart/form-data') { 171 + return 'formData'; 172 + } 173 + 174 + if ( 175 + ['application/', 'audio/', 'image/', 'video/'].some((type) => 176 + cleanContent.startsWith(type), 177 + ) 178 + ) { 179 + return 'blob'; 180 + } 181 + 182 + if (cleanContent.startsWith('text/')) { 183 + return 'text'; 184 + } 185 + }; 186 + 187 + export const setAuthParams = async ({ 188 + security, 189 + ...options 190 + }: Pick<Required<RequestOptions>, 'security'> & 191 + Pick<RequestOptions, 'auth' | 'query'> & { 192 + headers: Headers; 193 + }) => { 194 + for (const auth of security) { 195 + const token = await getAuthToken(auth, options.auth); 196 + 197 + if (!token) { 198 + continue; 199 + } 200 + 201 + const name = auth.name ?? 'Authorization'; 202 + 203 + switch (auth.in) { 204 + case 'query': 205 + if (!options.query) { 206 + options.query = {}; 207 + } 208 + options.query[name] = token; 209 + break; 210 + case 'cookie': 211 + options.headers.append('Cookie', `${name}=${token}`); 212 + break; 213 + case 'header': 214 + default: 215 + options.headers.set(name, token); 216 + break; 217 + } 218 + 219 + return; 220 + } 221 + }; 222 + 223 + export const buildUrl: Client['buildUrl'] = (options) => { 224 + const url = getUrl({ 225 + baseUrl: options.baseUrl as string, 226 + path: options.path, 227 + query: options.query, 228 + querySerializer: 229 + typeof options.querySerializer === 'function' 230 + ? options.querySerializer 231 + : createQuerySerializer(options.querySerializer), 232 + url: options.url, 233 + }); 234 + return url; 235 + }; 236 + 237 + export const getUrl = ({ 238 + baseUrl, 239 + path, 240 + query, 241 + querySerializer, 242 + url: _url, 243 + }: { 244 + baseUrl?: string; 245 + path?: Record<string, unknown>; 246 + query?: Record<string, unknown>; 247 + querySerializer: QuerySerializer; 248 + url: string; 249 + }) => { 250 + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; 251 + let url = (baseUrl ?? '') + pathUrl; 252 + if (path) { 253 + url = defaultPathSerializer({ path, url }); 254 + } 255 + let search = query ? querySerializer(query) : ''; 256 + if (search.startsWith('?')) { 257 + search = search.substring(1); 258 + } 259 + if (search) { 260 + url += `?${search}`; 261 + } 262 + return url; 263 + }; 264 + 265 + export const mergeConfigs = (a: Config, b: Config): Config => { 266 + const config = { ...a, ...b }; 267 + if (config.baseUrl?.endsWith('/')) { 268 + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); 269 + } 270 + config.headers = mergeHeaders(a.headers, b.headers); 271 + return config; 272 + }; 273 + 274 + export const mergeHeaders = ( 275 + ...headers: Array<Required<Config>['headers'] | undefined> 276 + ): Headers => { 277 + const mergedHeaders = new Headers(); 278 + for (const header of headers) { 279 + if (!header || typeof header !== 'object') { 280 + continue; 281 + } 282 + 283 + const iterator = 284 + header instanceof Headers ? header.entries() : Object.entries(header); 285 + 286 + for (const [key, value] of iterator) { 287 + if (value === null) { 288 + mergedHeaders.delete(key); 289 + } else if (Array.isArray(value)) { 290 + for (const v of value) { 291 + mergedHeaders.append(key, v as string); 292 + } 293 + } else if (value !== undefined) { 294 + // assume object headers are meant to be JSON stringified, i.e. their 295 + // content value in OpenAPI specification is 'application/json' 296 + mergedHeaders.set( 297 + key, 298 + typeof value === 'object' ? JSON.stringify(value) : (value as string), 299 + ); 300 + } 301 + } 302 + } 303 + return mergedHeaders; 304 + }; 305 + 306 + type ErrInterceptor<Err, Res, Req, Options> = ( 307 + error: Err, 308 + response: Res, 309 + request: Req, 310 + options: Options, 311 + ) => Err | Promise<Err>; 312 + 313 + type ReqInterceptor<Req, Options> = ( 314 + request: Req, 315 + options: Options, 316 + ) => Req | Promise<Req>; 317 + 318 + type ResInterceptor<Res, Req, Options> = ( 319 + response: Res, 320 + request: Req, 321 + options: Options, 322 + ) => Res | Promise<Res>; 323 + 324 + class Interceptors<Interceptor> { 325 + _fns: (Interceptor | null)[]; 326 + 327 + constructor() { 328 + this._fns = []; 329 + } 330 + 331 + clear() { 332 + this._fns = []; 333 + } 334 + 335 + getInterceptorIndex(id: number | Interceptor): number { 336 + if (typeof id === 'number') { 337 + return this._fns[id] ? id : -1; 338 + } else { 339 + return this._fns.indexOf(id); 340 + } 341 + } 342 + exists(id: number | Interceptor) { 343 + const index = this.getInterceptorIndex(id); 344 + return !!this._fns[index]; 345 + } 346 + 347 + eject(id: number | Interceptor) { 348 + const index = this.getInterceptorIndex(id); 349 + if (this._fns[index]) { 350 + this._fns[index] = null; 351 + } 352 + } 353 + 354 + update(id: number | Interceptor, fn: Interceptor) { 355 + const index = this.getInterceptorIndex(id); 356 + if (this._fns[index]) { 357 + this._fns[index] = fn; 358 + return id; 359 + } else { 360 + return false; 361 + } 362 + } 363 + 364 + use(fn: Interceptor) { 365 + this._fns = [...this._fns, fn]; 366 + return this._fns.length - 1; 367 + } 368 + } 369 + 370 + // `createInterceptors()` response, meant for external use as it does not 371 + // expose internals 372 + export interface Middleware<Req, Res, Err, Options> { 373 + error: Pick< 374 + Interceptors<ErrInterceptor<Err, Res, Req, Options>>, 375 + 'eject' | 'use' 376 + >; 377 + request: Pick<Interceptors<ReqInterceptor<Req, Options>>, 'eject' | 'use'>; 378 + response: Pick< 379 + Interceptors<ResInterceptor<Res, Req, Options>>, 380 + 'eject' | 'use' 381 + >; 382 + } 383 + 384 + // do not add `Middleware` as return type so we can use _fns internally 385 + export const createInterceptors = <Req, Res, Err, Options>() => ({ 386 + error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(), 387 + request: new Interceptors<ReqInterceptor<Req, Options>>(), 388 + response: new Interceptors<ResInterceptor<Res, Req, Options>>(), 389 + }); 390 + 391 + const defaultQuerySerializer = createQuerySerializer({ 392 + allowReserved: false, 393 + array: { 394 + explode: true, 395 + style: 'form', 396 + }, 397 + object: { 398 + explode: true, 399 + style: 'deepObject', 400 + }, 401 + }); 402 + 403 + const defaultHeaders = { 404 + 'Content-Type': 'application/json', 405 + }; 406 + 407 + export const createConfig = <T extends ClientOptions = ClientOptions>( 408 + override: Config<Omit<ClientOptions, keyof T> & T> = {}, 409 + ): Config<Omit<ClientOptions, keyof T> & T> => ({ 410 + ...jsonBodySerializer, 411 + headers: defaultHeaders, 412 + parseAs: 'auto', 413 + querySerializer: defaultQuerySerializer, 414 + ...override, 415 + });
+40
examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/auth.ts
··· 1 + export type AuthToken = string | undefined; 2 + 3 + export interface Auth { 4 + /** 5 + * Which part of the request do we use to send the auth? 6 + * 7 + * @default 'header' 8 + */ 9 + in?: 'header' | 'query' | 'cookie'; 10 + /** 11 + * Header or query parameter name. 12 + * 13 + * @default 'Authorization' 14 + */ 15 + name?: string; 16 + scheme?: 'basic' | 'bearer'; 17 + type: 'apiKey' | 'http'; 18 + } 19 + 20 + export const getAuthToken = async ( 21 + auth: Auth, 22 + callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken, 23 + ): Promise<string | undefined> => { 24 + const token = 25 + typeof callback === 'function' ? await callback(auth) : callback; 26 + 27 + if (!token) { 28 + return; 29 + } 30 + 31 + if (auth.scheme === 'bearer') { 32 + return `Bearer ${token}`; 33 + } 34 + 35 + if (auth.scheme === 'basic') { 36 + return `Basic ${btoa(token)}`; 37 + } 38 + 39 + return token; 40 + };
+84
examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/bodySerializer.ts
··· 1 + import type { 2 + ArrayStyle, 3 + ObjectStyle, 4 + SerializerOptions, 5 + } from './pathSerializer'; 6 + 7 + export type QuerySerializer = (query: Record<string, unknown>) => string; 8 + 9 + export type BodySerializer = (body: any) => any; 10 + 11 + export interface QuerySerializerOptions { 12 + allowReserved?: boolean; 13 + array?: SerializerOptions<ArrayStyle>; 14 + object?: SerializerOptions<ObjectStyle>; 15 + } 16 + 17 + const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { 18 + if (typeof value === 'string' || value instanceof Blob) { 19 + data.append(key, value); 20 + } else { 21 + data.append(key, JSON.stringify(value)); 22 + } 23 + }; 24 + 25 + const serializeUrlSearchParamsPair = ( 26 + data: URLSearchParams, 27 + key: string, 28 + value: unknown, 29 + ) => { 30 + if (typeof value === 'string') { 31 + data.append(key, value); 32 + } else { 33 + data.append(key, JSON.stringify(value)); 34 + } 35 + }; 36 + 37 + export const formDataBodySerializer = { 38 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 39 + body: T, 40 + ) => { 41 + const data = new FormData(); 42 + 43 + Object.entries(body).forEach(([key, value]) => { 44 + if (value === undefined || value === null) { 45 + return; 46 + } 47 + if (Array.isArray(value)) { 48 + value.forEach((v) => serializeFormDataPair(data, key, v)); 49 + } else { 50 + serializeFormDataPair(data, key, value); 51 + } 52 + }); 53 + 54 + return data; 55 + }, 56 + }; 57 + 58 + export const jsonBodySerializer = { 59 + bodySerializer: <T>(body: T) => 60 + JSON.stringify(body, (key, value) => 61 + typeof value === 'bigint' ? value.toString() : value, 62 + ), 63 + }; 64 + 65 + export const urlSearchParamsBodySerializer = { 66 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 67 + body: T, 68 + ) => { 69 + const data = new URLSearchParams(); 70 + 71 + Object.entries(body).forEach(([key, value]) => { 72 + if (value === undefined || value === null) { 73 + return; 74 + } 75 + if (Array.isArray(value)) { 76 + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); 77 + } else { 78 + serializeUrlSearchParamsPair(data, key, value); 79 + } 80 + }); 81 + 82 + return data.toString(); 83 + }, 84 + };
+141
examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/params.ts
··· 1 + type Slot = 'body' | 'headers' | 'path' | 'query'; 2 + 3 + export type Field = 4 + | { 5 + in: Exclude<Slot, 'body'>; 6 + key: string; 7 + map?: string; 8 + } 9 + | { 10 + in: Extract<Slot, 'body'>; 11 + key?: string; 12 + map?: string; 13 + }; 14 + 15 + export interface Fields { 16 + allowExtra?: Partial<Record<Slot, boolean>>; 17 + args?: ReadonlyArray<Field>; 18 + } 19 + 20 + export type FieldsConfig = ReadonlyArray<Field | Fields>; 21 + 22 + const extraPrefixesMap: Record<string, Slot> = { 23 + $body_: 'body', 24 + $headers_: 'headers', 25 + $path_: 'path', 26 + $query_: 'query', 27 + }; 28 + const extraPrefixes = Object.entries(extraPrefixesMap); 29 + 30 + type KeyMap = Map< 31 + string, 32 + { 33 + in: Slot; 34 + map?: string; 35 + } 36 + >; 37 + 38 + const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { 39 + if (!map) { 40 + map = new Map(); 41 + } 42 + 43 + for (const config of fields) { 44 + if ('in' in config) { 45 + if (config.key) { 46 + map.set(config.key, { 47 + in: config.in, 48 + map: config.map, 49 + }); 50 + } 51 + } else if (config.args) { 52 + buildKeyMap(config.args, map); 53 + } 54 + } 55 + 56 + return map; 57 + }; 58 + 59 + interface Params { 60 + body: unknown; 61 + headers: Record<string, unknown>; 62 + path: Record<string, unknown>; 63 + query: Record<string, unknown>; 64 + } 65 + 66 + const stripEmptySlots = (params: Params) => { 67 + for (const [slot, value] of Object.entries(params)) { 68 + if (value && typeof value === 'object' && !Object.keys(value).length) { 69 + delete params[slot as Slot]; 70 + } 71 + } 72 + }; 73 + 74 + export const buildClientParams = ( 75 + args: ReadonlyArray<unknown>, 76 + fields: FieldsConfig, 77 + ) => { 78 + const params: Params = { 79 + body: {}, 80 + headers: {}, 81 + path: {}, 82 + query: {}, 83 + }; 84 + 85 + const map = buildKeyMap(fields); 86 + 87 + let config: FieldsConfig[number] | undefined; 88 + 89 + for (const [index, arg] of args.entries()) { 90 + if (fields[index]) { 91 + config = fields[index]; 92 + } 93 + 94 + if (!config) { 95 + continue; 96 + } 97 + 98 + if ('in' in config) { 99 + if (config.key) { 100 + const field = map.get(config.key)!; 101 + const name = field.map || config.key; 102 + (params[field.in] as Record<string, unknown>)[name] = arg; 103 + } else { 104 + params.body = arg; 105 + } 106 + } else { 107 + for (const [key, value] of Object.entries(arg ?? {})) { 108 + const field = map.get(key); 109 + 110 + if (field) { 111 + const name = field.map || key; 112 + (params[field.in] as Record<string, unknown>)[name] = value; 113 + } else { 114 + const extra = extraPrefixes.find(([prefix]) => 115 + key.startsWith(prefix), 116 + ); 117 + 118 + if (extra) { 119 + const [prefix, slot] = extra; 120 + (params[slot] as Record<string, unknown>)[ 121 + key.slice(prefix.length) 122 + ] = value; 123 + } else { 124 + for (const [slot, allowed] of Object.entries( 125 + config.allowExtra ?? {}, 126 + )) { 127 + if (allowed) { 128 + (params[slot as Slot] as Record<string, unknown>)[key] = value; 129 + break; 130 + } 131 + } 132 + } 133 + } 134 + } 135 + } 136 + } 137 + 138 + stripEmptySlots(params); 139 + 140 + return params; 141 + };
+179
examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/pathSerializer.ts
··· 1 + interface SerializeOptions<T> 2 + extends SerializePrimitiveOptions, 3 + SerializerOptions<T> {} 4 + 5 + interface SerializePrimitiveOptions { 6 + allowReserved?: boolean; 7 + name: string; 8 + } 9 + 10 + export interface SerializerOptions<T> { 11 + /** 12 + * @default true 13 + */ 14 + explode: boolean; 15 + style: T; 16 + } 17 + 18 + export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; 19 + export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 20 + type MatrixStyle = 'label' | 'matrix' | 'simple'; 21 + export type ObjectStyle = 'form' | 'deepObject'; 22 + type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; 23 + 24 + interface SerializePrimitiveParam extends SerializePrimitiveOptions { 25 + value: string; 26 + } 27 + 28 + export const separatorArrayExplode = (style: ArraySeparatorStyle) => { 29 + switch (style) { 30 + case 'label': 31 + return '.'; 32 + case 'matrix': 33 + return ';'; 34 + case 'simple': 35 + return ','; 36 + default: 37 + return '&'; 38 + } 39 + }; 40 + 41 + export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { 42 + switch (style) { 43 + case 'form': 44 + return ','; 45 + case 'pipeDelimited': 46 + return '|'; 47 + case 'spaceDelimited': 48 + return '%20'; 49 + default: 50 + return ','; 51 + } 52 + }; 53 + 54 + export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { 55 + switch (style) { 56 + case 'label': 57 + return '.'; 58 + case 'matrix': 59 + return ';'; 60 + case 'simple': 61 + return ','; 62 + default: 63 + return '&'; 64 + } 65 + }; 66 + 67 + export const serializeArrayParam = ({ 68 + allowReserved, 69 + explode, 70 + name, 71 + style, 72 + value, 73 + }: SerializeOptions<ArraySeparatorStyle> & { 74 + value: unknown[]; 75 + }) => { 76 + if (!explode) { 77 + const joinedValues = ( 78 + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) 79 + ).join(separatorArrayNoExplode(style)); 80 + switch (style) { 81 + case 'label': 82 + return `.${joinedValues}`; 83 + case 'matrix': 84 + return `;${name}=${joinedValues}`; 85 + case 'simple': 86 + return joinedValues; 87 + default: 88 + return `${name}=${joinedValues}`; 89 + } 90 + } 91 + 92 + const separator = separatorArrayExplode(style); 93 + const joinedValues = value 94 + .map((v) => { 95 + if (style === 'label' || style === 'simple') { 96 + return allowReserved ? v : encodeURIComponent(v as string); 97 + } 98 + 99 + return serializePrimitiveParam({ 100 + allowReserved, 101 + name, 102 + value: v as string, 103 + }); 104 + }) 105 + .join(separator); 106 + return style === 'label' || style === 'matrix' 107 + ? separator + joinedValues 108 + : joinedValues; 109 + }; 110 + 111 + export const serializePrimitiveParam = ({ 112 + allowReserved, 113 + name, 114 + value, 115 + }: SerializePrimitiveParam) => { 116 + if (value === undefined || value === null) { 117 + return ''; 118 + } 119 + 120 + if (typeof value === 'object') { 121 + throw new Error( 122 + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', 123 + ); 124 + } 125 + 126 + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; 127 + }; 128 + 129 + export const serializeObjectParam = ({ 130 + allowReserved, 131 + explode, 132 + name, 133 + style, 134 + value, 135 + valueOnly, 136 + }: SerializeOptions<ObjectSeparatorStyle> & { 137 + value: Record<string, unknown> | Date; 138 + valueOnly?: boolean; 139 + }) => { 140 + if (value instanceof Date) { 141 + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; 142 + } 143 + 144 + if (style !== 'deepObject' && !explode) { 145 + let values: string[] = []; 146 + Object.entries(value).forEach(([key, v]) => { 147 + values = [ 148 + ...values, 149 + key, 150 + allowReserved ? (v as string) : encodeURIComponent(v as string), 151 + ]; 152 + }); 153 + const joinedValues = values.join(','); 154 + switch (style) { 155 + case 'form': 156 + return `${name}=${joinedValues}`; 157 + case 'label': 158 + return `.${joinedValues}`; 159 + case 'matrix': 160 + return `;${name}=${joinedValues}`; 161 + default: 162 + return joinedValues; 163 + } 164 + } 165 + 166 + const separator = separatorObjectExplode(style); 167 + const joinedValues = Object.entries(value) 168 + .map(([key, v]) => 169 + serializePrimitiveParam({ 170 + allowReserved, 171 + name: style === 'deepObject' ? `${name}[${key}]` : key, 172 + value: v as string, 173 + }), 174 + ) 175 + .join(separator); 176 + return style === 'label' || style === 'matrix' 177 + ? separator + joinedValues 178 + : joinedValues; 179 + };
+98
examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/types.ts
··· 1 + import type { Auth, AuthToken } from './auth'; 2 + import type { 3 + BodySerializer, 4 + QuerySerializer, 5 + QuerySerializerOptions, 6 + } from './bodySerializer'; 7 + 8 + export interface Client< 9 + RequestFn = never, 10 + Config = unknown, 11 + MethodFn = never, 12 + BuildUrlFn = never, 13 + > { 14 + /** 15 + * Returns the final request URL. 16 + */ 17 + buildUrl: BuildUrlFn; 18 + connect: MethodFn; 19 + delete: MethodFn; 20 + get: MethodFn; 21 + getConfig: () => Config; 22 + head: MethodFn; 23 + options: MethodFn; 24 + patch: MethodFn; 25 + post: MethodFn; 26 + put: MethodFn; 27 + request: RequestFn; 28 + setConfig: (config: Config) => Config; 29 + trace: MethodFn; 30 + } 31 + 32 + export interface Config { 33 + /** 34 + * Auth token or a function returning auth token. The resolved value will be 35 + * added to the request payload as defined by its `security` array. 36 + */ 37 + auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken; 38 + /** 39 + * A function for serializing request body parameter. By default, 40 + * {@link JSON.stringify()} will be used. 41 + */ 42 + bodySerializer?: BodySerializer | null; 43 + /** 44 + * An object containing any HTTP headers that you want to pre-populate your 45 + * `Headers` object with. 46 + * 47 + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} 48 + */ 49 + headers?: 50 + | RequestInit['headers'] 51 + | Record< 52 + string, 53 + | string 54 + | number 55 + | boolean 56 + | (string | number | boolean)[] 57 + | null 58 + | undefined 59 + | unknown 60 + >; 61 + /** 62 + * The request method. 63 + * 64 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} 65 + */ 66 + method?: 67 + | 'CONNECT' 68 + | 'DELETE' 69 + | 'GET' 70 + | 'HEAD' 71 + | 'OPTIONS' 72 + | 'PATCH' 73 + | 'POST' 74 + | 'PUT' 75 + | 'TRACE'; 76 + /** 77 + * A function for serializing request query parameters. By default, arrays 78 + * will be exploded in form style, objects will be exploded in deepObject 79 + * style, and reserved characters are percent-encoded. 80 + * 81 + * This method will have no effect if the native `paramsSerializer()` Axios 82 + * API function is used. 83 + * 84 + * {@link https://swagger.io/docs/specification/serialization/#query View examples} 85 + */ 86 + querySerializer?: QuerySerializer | QuerySerializerOptions; 87 + /** 88 + * A function transforming response data before it's returned. This is useful 89 + * for post-processing data, e.g. converting ISO strings into Date objects. 90 + */ 91 + responseTransformer?: (data: unknown) => Promise<unknown>; 92 + /** 93 + * A function validating response data. This is useful if you want to ensure 94 + * the response conforms to the desired shape, so it can be safely passed to 95 + * the transformers and returned to the user. 96 + */ 97 + responseValidator?: (data: unknown) => Promise<unknown>; 98 + }
+5 -10
examples/openapi-ts-tanstack-angular-query-experimental/src/client/sdk.gen.ts
··· 1 1 // This file is auto-generated by @hey-api/openapi-ts 2 2 3 - import type { 4 - Client, 5 - Options as ClientOptions, 6 - TDataShape, 7 - } from '@hey-api/client-fetch'; 8 - 3 + import type { Client, Options as ClientOptions, TDataShape } from './client'; 9 4 import { client as _heyApiClient } from './client.gen'; 10 5 import type { 11 6 AddPetData, ··· 106 101 ...options, 107 102 headers: { 108 103 'Content-Type': 'application/json', 109 - ...options?.headers, 104 + ...options.headers, 110 105 }, 111 106 }); 112 107 ··· 132 127 ...options, 133 128 headers: { 134 129 'Content-Type': 'application/json', 135 - ...options?.headers, 130 + ...options.headers, 136 131 }, 137 132 }); 138 133 ··· 273 268 ...options, 274 269 headers: { 275 270 'Content-Type': 'application/octet-stream', 276 - ...options?.headers, 271 + ...options.headers, 277 272 }, 278 273 }); 279 274 ··· 471 466 ...options, 472 467 headers: { 473 468 'Content-Type': 'application/json', 474 - ...options?.headers, 469 + ...options.headers, 475 470 }, 476 471 });
-1
examples/openapi-ts-tanstack-react-query/package.json
··· 12 12 "typecheck": "tsc --noEmit" 13 13 }, 14 14 "dependencies": { 15 - "@hey-api/client-fetch": "workspace:*", 16 15 "@radix-ui/react-form": "0.1.1", 17 16 "@radix-ui/react-icons": "1.3.2", 18 17 "@radix-ui/themes": "3.1.6",
+1 -1
examples/openapi-ts-tanstack-react-query/src/App.tsx
··· 1 1 import './App.css'; 2 2 3 - import { createClient } from '@hey-api/client-fetch'; 4 3 import * as Form from '@radix-ui/react-form'; 5 4 import { DownloadIcon, PlusIcon, ReloadIcon } from '@radix-ui/react-icons'; 6 5 import { ··· 23 22 getPetByIdOptions, 24 23 updatePetMutation, 25 24 } from './client/@tanstack/react-query.gen'; 25 + import { createClient } from './client/client'; 26 26 import { PetSchema } from './client/schemas.gen'; 27 27 import type { Pet } from './client/types.gen'; 28 28
+1 -2
examples/openapi-ts-tanstack-react-query/src/client/client.gen.ts
··· 5 5 type Config, 6 6 createClient, 7 7 createConfig, 8 - } from '@hey-api/client-fetch'; 9 - 8 + } from './client'; 10 9 import type { ClientOptions } from './types.gen'; 11 10 12 11 /**
+181
examples/openapi-ts-tanstack-react-query/src/client/client/client.ts
··· 1 + import type { Client, Config, RequestOptions } from './types'; 2 + import { 3 + buildUrl, 4 + createConfig, 5 + createInterceptors, 6 + getParseAs, 7 + mergeConfigs, 8 + mergeHeaders, 9 + setAuthParams, 10 + } from './utils'; 11 + 12 + type ReqInit = Omit<RequestInit, 'body' | 'headers'> & { 13 + body?: any; 14 + headers: ReturnType<typeof mergeHeaders>; 15 + }; 16 + 17 + export const createClient = (config: Config = {}): Client => { 18 + let _config = mergeConfigs(createConfig(), config); 19 + 20 + const getConfig = (): Config => ({ ..._config }); 21 + 22 + const setConfig = (config: Config): Config => { 23 + _config = mergeConfigs(_config, config); 24 + return getConfig(); 25 + }; 26 + 27 + const interceptors = createInterceptors< 28 + Request, 29 + Response, 30 + unknown, 31 + RequestOptions 32 + >(); 33 + 34 + const request: Client['request'] = async (options) => { 35 + const opts = { 36 + ..._config, 37 + ...options, 38 + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, 39 + headers: mergeHeaders(_config.headers, options.headers), 40 + }; 41 + 42 + if (opts.security) { 43 + await setAuthParams({ 44 + ...opts, 45 + security: opts.security, 46 + }); 47 + } 48 + 49 + if (opts.body && opts.bodySerializer) { 50 + opts.body = opts.bodySerializer(opts.body); 51 + } 52 + 53 + // remove Content-Type header if body is empty to avoid sending invalid requests 54 + if (opts.body === undefined || opts.body === '') { 55 + opts.headers.delete('Content-Type'); 56 + } 57 + 58 + const url = buildUrl(opts); 59 + const requestInit: ReqInit = { 60 + redirect: 'follow', 61 + ...opts, 62 + }; 63 + 64 + let request = new Request(url, requestInit); 65 + 66 + for (const fn of interceptors.request._fns) { 67 + if (fn) { 68 + request = await fn(request, opts); 69 + } 70 + } 71 + 72 + // fetch must be assigned here, otherwise it would throw the error: 73 + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation 74 + const _fetch = opts.fetch!; 75 + let response = await _fetch(request); 76 + 77 + for (const fn of interceptors.response._fns) { 78 + if (fn) { 79 + response = await fn(response, request, opts); 80 + } 81 + } 82 + 83 + const result = { 84 + request, 85 + response, 86 + }; 87 + 88 + if (response.ok) { 89 + if ( 90 + response.status === 204 || 91 + response.headers.get('Content-Length') === '0' 92 + ) { 93 + return opts.responseStyle === 'data' 94 + ? {} 95 + : { 96 + data: {}, 97 + ...result, 98 + }; 99 + } 100 + 101 + const parseAs = 102 + (opts.parseAs === 'auto' 103 + ? getParseAs(response.headers.get('Content-Type')) 104 + : opts.parseAs) ?? 'json'; 105 + 106 + if (parseAs === 'stream') { 107 + return opts.responseStyle === 'data' 108 + ? response.body 109 + : { 110 + data: response.body, 111 + ...result, 112 + }; 113 + } 114 + 115 + let data = await response[parseAs](); 116 + if (parseAs === 'json') { 117 + if (opts.responseValidator) { 118 + await opts.responseValidator(data); 119 + } 120 + 121 + if (opts.responseTransformer) { 122 + data = await opts.responseTransformer(data); 123 + } 124 + } 125 + 126 + return opts.responseStyle === 'data' 127 + ? data 128 + : { 129 + data, 130 + ...result, 131 + }; 132 + } 133 + 134 + let error = await response.text(); 135 + 136 + try { 137 + error = JSON.parse(error); 138 + } catch { 139 + // noop 140 + } 141 + 142 + let finalError = error; 143 + 144 + for (const fn of interceptors.error._fns) { 145 + if (fn) { 146 + finalError = (await fn(error, response, request, opts)) as string; 147 + } 148 + } 149 + 150 + finalError = finalError || ({} as string); 151 + 152 + if (opts.throwOnError) { 153 + throw finalError; 154 + } 155 + 156 + // TODO: we probably want to return error and improve types 157 + return opts.responseStyle === 'data' 158 + ? undefined 159 + : { 160 + error: finalError, 161 + ...result, 162 + }; 163 + }; 164 + 165 + return { 166 + buildUrl, 167 + connect: (options) => request({ ...options, method: 'CONNECT' }), 168 + delete: (options) => request({ ...options, method: 'DELETE' }), 169 + get: (options) => request({ ...options, method: 'GET' }), 170 + getConfig, 171 + head: (options) => request({ ...options, method: 'HEAD' }), 172 + interceptors, 173 + options: (options) => request({ ...options, method: 'OPTIONS' }), 174 + patch: (options) => request({ ...options, method: 'PATCH' }), 175 + post: (options) => request({ ...options, method: 'POST' }), 176 + put: (options) => request({ ...options, method: 'PUT' }), 177 + request, 178 + setConfig, 179 + trace: (options) => request({ ...options, method: 'TRACE' }), 180 + }; 181 + };
+22
examples/openapi-ts-tanstack-react-query/src/client/client/index.ts
··· 1 + export type { Auth } from '../core/auth'; 2 + export type { QuerySerializerOptions } from '../core/bodySerializer'; 3 + export { 4 + formDataBodySerializer, 5 + jsonBodySerializer, 6 + urlSearchParamsBodySerializer, 7 + } from '../core/bodySerializer'; 8 + export { buildClientParams } from '../core/params'; 9 + export { createClient } from './client'; 10 + export type { 11 + Client, 12 + ClientOptions, 13 + Config, 14 + CreateClientConfig, 15 + Options, 16 + OptionsLegacyParser, 17 + RequestOptions, 18 + RequestResult, 19 + ResponseStyle, 20 + TDataShape, 21 + } from './types'; 22 + export { createConfig, mergeHeaders } from './utils';
+212
examples/openapi-ts-tanstack-react-query/src/client/client/types.ts
··· 1 + import type { Auth } from '../core/auth'; 2 + import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; 3 + import type { Middleware } from './utils'; 4 + 5 + export type ResponseStyle = 'data' | 'fields'; 6 + 7 + export interface Config<T extends ClientOptions = ClientOptions> 8 + extends Omit<RequestInit, 'body' | 'headers' | 'method'>, 9 + CoreConfig { 10 + /** 11 + * Base URL for all requests made by this client. 12 + */ 13 + baseUrl?: T['baseUrl']; 14 + /** 15 + * Fetch API implementation. You can use this option to provide a custom 16 + * fetch instance. 17 + * 18 + * @default globalThis.fetch 19 + */ 20 + fetch?: (request: Request) => ReturnType<typeof fetch>; 21 + /** 22 + * Please don't use the Fetch client for Next.js applications. The `next` 23 + * options won't have any effect. 24 + * 25 + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. 26 + */ 27 + next?: never; 28 + /** 29 + * Return the response data parsed in a specified format. By default, `auto` 30 + * will infer the appropriate method from the `Content-Type` response header. 31 + * You can override this behavior with any of the {@link Body} methods. 32 + * Select `stream` if you don't want to parse response data at all. 33 + * 34 + * @default 'auto' 35 + */ 36 + parseAs?: Exclude<keyof Body, 'body' | 'bodyUsed'> | 'auto' | 'stream'; 37 + /** 38 + * Should we return only data or multiple fields (data, error, response, etc.)? 39 + * 40 + * @default 'fields' 41 + */ 42 + responseStyle?: ResponseStyle; 43 + /** 44 + * Throw an error instead of returning it in the response? 45 + * 46 + * @default false 47 + */ 48 + throwOnError?: T['throwOnError']; 49 + } 50 + 51 + export interface RequestOptions< 52 + TResponseStyle extends ResponseStyle = 'fields', 53 + ThrowOnError extends boolean = boolean, 54 + Url extends string = string, 55 + > extends Config<{ 56 + responseStyle: TResponseStyle; 57 + throwOnError: ThrowOnError; 58 + }> { 59 + /** 60 + * Any body that you want to add to your request. 61 + * 62 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} 63 + */ 64 + body?: unknown; 65 + path?: Record<string, unknown>; 66 + query?: Record<string, unknown>; 67 + /** 68 + * Security mechanism(s) to use for the request. 69 + */ 70 + security?: ReadonlyArray<Auth>; 71 + url: Url; 72 + } 73 + 74 + export type RequestResult< 75 + TData = unknown, 76 + TError = unknown, 77 + ThrowOnError extends boolean = boolean, 78 + TResponseStyle extends ResponseStyle = 'fields', 79 + > = ThrowOnError extends true 80 + ? Promise< 81 + TResponseStyle extends 'data' 82 + ? TData extends Record<string, unknown> 83 + ? TData[keyof TData] 84 + : TData 85 + : { 86 + data: TData extends Record<string, unknown> 87 + ? TData[keyof TData] 88 + : TData; 89 + request: Request; 90 + response: Response; 91 + } 92 + > 93 + : Promise< 94 + TResponseStyle extends 'data' 95 + ? 96 + | (TData extends Record<string, unknown> 97 + ? TData[keyof TData] 98 + : TData) 99 + | undefined 100 + : ( 101 + | { 102 + data: TData extends Record<string, unknown> 103 + ? TData[keyof TData] 104 + : TData; 105 + error: undefined; 106 + } 107 + | { 108 + data: undefined; 109 + error: TError extends Record<string, unknown> 110 + ? TError[keyof TError] 111 + : TError; 112 + } 113 + ) & { 114 + request: Request; 115 + response: Response; 116 + } 117 + >; 118 + 119 + export interface ClientOptions { 120 + baseUrl?: string; 121 + responseStyle?: ResponseStyle; 122 + throwOnError?: boolean; 123 + } 124 + 125 + type MethodFn = < 126 + TData = unknown, 127 + TError = unknown, 128 + ThrowOnError extends boolean = false, 129 + TResponseStyle extends ResponseStyle = 'fields', 130 + >( 131 + options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'>, 132 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 133 + 134 + type RequestFn = < 135 + TData = unknown, 136 + TError = unknown, 137 + ThrowOnError extends boolean = false, 138 + TResponseStyle extends ResponseStyle = 'fields', 139 + >( 140 + options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'> & 141 + Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, 'method'>, 142 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 143 + 144 + type BuildUrlFn = < 145 + TData extends { 146 + body?: unknown; 147 + path?: Record<string, unknown>; 148 + query?: Record<string, unknown>; 149 + url: string; 150 + }, 151 + >( 152 + options: Pick<TData, 'url'> & Options<TData>, 153 + ) => string; 154 + 155 + export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & { 156 + interceptors: Middleware<Request, Response, unknown, RequestOptions>; 157 + }; 158 + 159 + /** 160 + * The `createClientConfig()` function will be called on client initialization 161 + * and the returned object will become the client's initial configuration. 162 + * 163 + * You may want to initialize your client this way instead of calling 164 + * `setConfig()`. This is useful for example if you're using Next.js 165 + * to ensure your client always has the correct values. 166 + */ 167 + export type CreateClientConfig<T extends ClientOptions = ClientOptions> = ( 168 + override?: Config<ClientOptions & T>, 169 + ) => Config<Required<ClientOptions> & T>; 170 + 171 + export interface TDataShape { 172 + body?: unknown; 173 + headers?: unknown; 174 + path?: unknown; 175 + query?: unknown; 176 + url: string; 177 + } 178 + 179 + type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>; 180 + 181 + export type Options< 182 + TData extends TDataShape = TDataShape, 183 + ThrowOnError extends boolean = boolean, 184 + TResponseStyle extends ResponseStyle = 'fields', 185 + > = OmitKeys< 186 + RequestOptions<TResponseStyle, ThrowOnError>, 187 + 'body' | 'path' | 'query' | 'url' 188 + > & 189 + Omit<TData, 'url'>; 190 + 191 + export type OptionsLegacyParser< 192 + TData = unknown, 193 + ThrowOnError extends boolean = boolean, 194 + TResponseStyle extends ResponseStyle = 'fields', 195 + > = TData extends { body?: any } 196 + ? TData extends { headers?: any } 197 + ? OmitKeys< 198 + RequestOptions<TResponseStyle, ThrowOnError>, 199 + 'body' | 'headers' | 'url' 200 + > & 201 + TData 202 + : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'body' | 'url'> & 203 + TData & 204 + Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'headers'> 205 + : TData extends { headers?: any } 206 + ? OmitKeys< 207 + RequestOptions<TResponseStyle, ThrowOnError>, 208 + 'headers' | 'url' 209 + > & 210 + TData & 211 + Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'body'> 212 + : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'url'> & TData;
+415
examples/openapi-ts-tanstack-react-query/src/client/client/utils.ts
··· 1 + import { getAuthToken } from '../core/auth'; 2 + import type { 3 + QuerySerializer, 4 + QuerySerializerOptions, 5 + } from '../core/bodySerializer'; 6 + import { jsonBodySerializer } from '../core/bodySerializer'; 7 + import { 8 + serializeArrayParam, 9 + serializeObjectParam, 10 + serializePrimitiveParam, 11 + } from '../core/pathSerializer'; 12 + import type { Client, ClientOptions, Config, RequestOptions } from './types'; 13 + 14 + interface PathSerializer { 15 + path: Record<string, unknown>; 16 + url: string; 17 + } 18 + 19 + const PATH_PARAM_RE = /\{[^{}]+\}/g; 20 + 21 + type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; 22 + type MatrixStyle = 'label' | 'matrix' | 'simple'; 23 + type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 24 + 25 + const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { 26 + let url = _url; 27 + const matches = _url.match(PATH_PARAM_RE); 28 + if (matches) { 29 + for (const match of matches) { 30 + let explode = false; 31 + let name = match.substring(1, match.length - 1); 32 + let style: ArraySeparatorStyle = 'simple'; 33 + 34 + if (name.endsWith('*')) { 35 + explode = true; 36 + name = name.substring(0, name.length - 1); 37 + } 38 + 39 + if (name.startsWith('.')) { 40 + name = name.substring(1); 41 + style = 'label'; 42 + } else if (name.startsWith(';')) { 43 + name = name.substring(1); 44 + style = 'matrix'; 45 + } 46 + 47 + const value = path[name]; 48 + 49 + if (value === undefined || value === null) { 50 + continue; 51 + } 52 + 53 + if (Array.isArray(value)) { 54 + url = url.replace( 55 + match, 56 + serializeArrayParam({ explode, name, style, value }), 57 + ); 58 + continue; 59 + } 60 + 61 + if (typeof value === 'object') { 62 + url = url.replace( 63 + match, 64 + serializeObjectParam({ 65 + explode, 66 + name, 67 + style, 68 + value: value as Record<string, unknown>, 69 + valueOnly: true, 70 + }), 71 + ); 72 + continue; 73 + } 74 + 75 + if (style === 'matrix') { 76 + url = url.replace( 77 + match, 78 + `;${serializePrimitiveParam({ 79 + name, 80 + value: value as string, 81 + })}`, 82 + ); 83 + continue; 84 + } 85 + 86 + const replaceValue = encodeURIComponent( 87 + style === 'label' ? `.${value as string}` : (value as string), 88 + ); 89 + url = url.replace(match, replaceValue); 90 + } 91 + } 92 + return url; 93 + }; 94 + 95 + export const createQuerySerializer = <T = unknown>({ 96 + allowReserved, 97 + array, 98 + object, 99 + }: QuerySerializerOptions = {}) => { 100 + const querySerializer = (queryParams: T) => { 101 + const search: string[] = []; 102 + if (queryParams && typeof queryParams === 'object') { 103 + for (const name in queryParams) { 104 + const value = queryParams[name]; 105 + 106 + if (value === undefined || value === null) { 107 + continue; 108 + } 109 + 110 + if (Array.isArray(value)) { 111 + const serializedArray = serializeArrayParam({ 112 + allowReserved, 113 + explode: true, 114 + name, 115 + style: 'form', 116 + value, 117 + ...array, 118 + }); 119 + if (serializedArray) search.push(serializedArray); 120 + } else if (typeof value === 'object') { 121 + const serializedObject = serializeObjectParam({ 122 + allowReserved, 123 + explode: true, 124 + name, 125 + style: 'deepObject', 126 + value: value as Record<string, unknown>, 127 + ...object, 128 + }); 129 + if (serializedObject) search.push(serializedObject); 130 + } else { 131 + const serializedPrimitive = serializePrimitiveParam({ 132 + allowReserved, 133 + name, 134 + value: value as string, 135 + }); 136 + if (serializedPrimitive) search.push(serializedPrimitive); 137 + } 138 + } 139 + } 140 + return search.join('&'); 141 + }; 142 + return querySerializer; 143 + }; 144 + 145 + /** 146 + * Infers parseAs value from provided Content-Type header. 147 + */ 148 + export const getParseAs = ( 149 + contentType: string | null, 150 + ): Exclude<Config['parseAs'], 'auto'> => { 151 + if (!contentType) { 152 + // If no Content-Type header is provided, the best we can do is return the raw response body, 153 + // which is effectively the same as the 'stream' option. 154 + return 'stream'; 155 + } 156 + 157 + const cleanContent = contentType.split(';')[0]?.trim(); 158 + 159 + if (!cleanContent) { 160 + return; 161 + } 162 + 163 + if ( 164 + cleanContent.startsWith('application/json') || 165 + cleanContent.endsWith('+json') 166 + ) { 167 + return 'json'; 168 + } 169 + 170 + if (cleanContent === 'multipart/form-data') { 171 + return 'formData'; 172 + } 173 + 174 + if ( 175 + ['application/', 'audio/', 'image/', 'video/'].some((type) => 176 + cleanContent.startsWith(type), 177 + ) 178 + ) { 179 + return 'blob'; 180 + } 181 + 182 + if (cleanContent.startsWith('text/')) { 183 + return 'text'; 184 + } 185 + }; 186 + 187 + export const setAuthParams = async ({ 188 + security, 189 + ...options 190 + }: Pick<Required<RequestOptions>, 'security'> & 191 + Pick<RequestOptions, 'auth' | 'query'> & { 192 + headers: Headers; 193 + }) => { 194 + for (const auth of security) { 195 + const token = await getAuthToken(auth, options.auth); 196 + 197 + if (!token) { 198 + continue; 199 + } 200 + 201 + const name = auth.name ?? 'Authorization'; 202 + 203 + switch (auth.in) { 204 + case 'query': 205 + if (!options.query) { 206 + options.query = {}; 207 + } 208 + options.query[name] = token; 209 + break; 210 + case 'cookie': 211 + options.headers.append('Cookie', `${name}=${token}`); 212 + break; 213 + case 'header': 214 + default: 215 + options.headers.set(name, token); 216 + break; 217 + } 218 + 219 + return; 220 + } 221 + }; 222 + 223 + export const buildUrl: Client['buildUrl'] = (options) => { 224 + const url = getUrl({ 225 + baseUrl: options.baseUrl as string, 226 + path: options.path, 227 + query: options.query, 228 + querySerializer: 229 + typeof options.querySerializer === 'function' 230 + ? options.querySerializer 231 + : createQuerySerializer(options.querySerializer), 232 + url: options.url, 233 + }); 234 + return url; 235 + }; 236 + 237 + export const getUrl = ({ 238 + baseUrl, 239 + path, 240 + query, 241 + querySerializer, 242 + url: _url, 243 + }: { 244 + baseUrl?: string; 245 + path?: Record<string, unknown>; 246 + query?: Record<string, unknown>; 247 + querySerializer: QuerySerializer; 248 + url: string; 249 + }) => { 250 + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; 251 + let url = (baseUrl ?? '') + pathUrl; 252 + if (path) { 253 + url = defaultPathSerializer({ path, url }); 254 + } 255 + let search = query ? querySerializer(query) : ''; 256 + if (search.startsWith('?')) { 257 + search = search.substring(1); 258 + } 259 + if (search) { 260 + url += `?${search}`; 261 + } 262 + return url; 263 + }; 264 + 265 + export const mergeConfigs = (a: Config, b: Config): Config => { 266 + const config = { ...a, ...b }; 267 + if (config.baseUrl?.endsWith('/')) { 268 + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); 269 + } 270 + config.headers = mergeHeaders(a.headers, b.headers); 271 + return config; 272 + }; 273 + 274 + export const mergeHeaders = ( 275 + ...headers: Array<Required<Config>['headers'] | undefined> 276 + ): Headers => { 277 + const mergedHeaders = new Headers(); 278 + for (const header of headers) { 279 + if (!header || typeof header !== 'object') { 280 + continue; 281 + } 282 + 283 + const iterator = 284 + header instanceof Headers ? header.entries() : Object.entries(header); 285 + 286 + for (const [key, value] of iterator) { 287 + if (value === null) { 288 + mergedHeaders.delete(key); 289 + } else if (Array.isArray(value)) { 290 + for (const v of value) { 291 + mergedHeaders.append(key, v as string); 292 + } 293 + } else if (value !== undefined) { 294 + // assume object headers are meant to be JSON stringified, i.e. their 295 + // content value in OpenAPI specification is 'application/json' 296 + mergedHeaders.set( 297 + key, 298 + typeof value === 'object' ? JSON.stringify(value) : (value as string), 299 + ); 300 + } 301 + } 302 + } 303 + return mergedHeaders; 304 + }; 305 + 306 + type ErrInterceptor<Err, Res, Req, Options> = ( 307 + error: Err, 308 + response: Res, 309 + request: Req, 310 + options: Options, 311 + ) => Err | Promise<Err>; 312 + 313 + type ReqInterceptor<Req, Options> = ( 314 + request: Req, 315 + options: Options, 316 + ) => Req | Promise<Req>; 317 + 318 + type ResInterceptor<Res, Req, Options> = ( 319 + response: Res, 320 + request: Req, 321 + options: Options, 322 + ) => Res | Promise<Res>; 323 + 324 + class Interceptors<Interceptor> { 325 + _fns: (Interceptor | null)[]; 326 + 327 + constructor() { 328 + this._fns = []; 329 + } 330 + 331 + clear() { 332 + this._fns = []; 333 + } 334 + 335 + getInterceptorIndex(id: number | Interceptor): number { 336 + if (typeof id === 'number') { 337 + return this._fns[id] ? id : -1; 338 + } else { 339 + return this._fns.indexOf(id); 340 + } 341 + } 342 + exists(id: number | Interceptor) { 343 + const index = this.getInterceptorIndex(id); 344 + return !!this._fns[index]; 345 + } 346 + 347 + eject(id: number | Interceptor) { 348 + const index = this.getInterceptorIndex(id); 349 + if (this._fns[index]) { 350 + this._fns[index] = null; 351 + } 352 + } 353 + 354 + update(id: number | Interceptor, fn: Interceptor) { 355 + const index = this.getInterceptorIndex(id); 356 + if (this._fns[index]) { 357 + this._fns[index] = fn; 358 + return id; 359 + } else { 360 + return false; 361 + } 362 + } 363 + 364 + use(fn: Interceptor) { 365 + this._fns = [...this._fns, fn]; 366 + return this._fns.length - 1; 367 + } 368 + } 369 + 370 + // `createInterceptors()` response, meant for external use as it does not 371 + // expose internals 372 + export interface Middleware<Req, Res, Err, Options> { 373 + error: Pick< 374 + Interceptors<ErrInterceptor<Err, Res, Req, Options>>, 375 + 'eject' | 'use' 376 + >; 377 + request: Pick<Interceptors<ReqInterceptor<Req, Options>>, 'eject' | 'use'>; 378 + response: Pick< 379 + Interceptors<ResInterceptor<Res, Req, Options>>, 380 + 'eject' | 'use' 381 + >; 382 + } 383 + 384 + // do not add `Middleware` as return type so we can use _fns internally 385 + export const createInterceptors = <Req, Res, Err, Options>() => ({ 386 + error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(), 387 + request: new Interceptors<ReqInterceptor<Req, Options>>(), 388 + response: new Interceptors<ResInterceptor<Res, Req, Options>>(), 389 + }); 390 + 391 + const defaultQuerySerializer = createQuerySerializer({ 392 + allowReserved: false, 393 + array: { 394 + explode: true, 395 + style: 'form', 396 + }, 397 + object: { 398 + explode: true, 399 + style: 'deepObject', 400 + }, 401 + }); 402 + 403 + const defaultHeaders = { 404 + 'Content-Type': 'application/json', 405 + }; 406 + 407 + export const createConfig = <T extends ClientOptions = ClientOptions>( 408 + override: Config<Omit<ClientOptions, keyof T> & T> = {}, 409 + ): Config<Omit<ClientOptions, keyof T> & T> => ({ 410 + ...jsonBodySerializer, 411 + headers: defaultHeaders, 412 + parseAs: 'auto', 413 + querySerializer: defaultQuerySerializer, 414 + ...override, 415 + });
+40
examples/openapi-ts-tanstack-react-query/src/client/core/auth.ts
··· 1 + export type AuthToken = string | undefined; 2 + 3 + export interface Auth { 4 + /** 5 + * Which part of the request do we use to send the auth? 6 + * 7 + * @default 'header' 8 + */ 9 + in?: 'header' | 'query' | 'cookie'; 10 + /** 11 + * Header or query parameter name. 12 + * 13 + * @default 'Authorization' 14 + */ 15 + name?: string; 16 + scheme?: 'basic' | 'bearer'; 17 + type: 'apiKey' | 'http'; 18 + } 19 + 20 + export const getAuthToken = async ( 21 + auth: Auth, 22 + callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken, 23 + ): Promise<string | undefined> => { 24 + const token = 25 + typeof callback === 'function' ? await callback(auth) : callback; 26 + 27 + if (!token) { 28 + return; 29 + } 30 + 31 + if (auth.scheme === 'bearer') { 32 + return `Bearer ${token}`; 33 + } 34 + 35 + if (auth.scheme === 'basic') { 36 + return `Basic ${btoa(token)}`; 37 + } 38 + 39 + return token; 40 + };
+84
examples/openapi-ts-tanstack-react-query/src/client/core/bodySerializer.ts
··· 1 + import type { 2 + ArrayStyle, 3 + ObjectStyle, 4 + SerializerOptions, 5 + } from './pathSerializer'; 6 + 7 + export type QuerySerializer = (query: Record<string, unknown>) => string; 8 + 9 + export type BodySerializer = (body: any) => any; 10 + 11 + export interface QuerySerializerOptions { 12 + allowReserved?: boolean; 13 + array?: SerializerOptions<ArrayStyle>; 14 + object?: SerializerOptions<ObjectStyle>; 15 + } 16 + 17 + const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { 18 + if (typeof value === 'string' || value instanceof Blob) { 19 + data.append(key, value); 20 + } else { 21 + data.append(key, JSON.stringify(value)); 22 + } 23 + }; 24 + 25 + const serializeUrlSearchParamsPair = ( 26 + data: URLSearchParams, 27 + key: string, 28 + value: unknown, 29 + ) => { 30 + if (typeof value === 'string') { 31 + data.append(key, value); 32 + } else { 33 + data.append(key, JSON.stringify(value)); 34 + } 35 + }; 36 + 37 + export const formDataBodySerializer = { 38 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 39 + body: T, 40 + ) => { 41 + const data = new FormData(); 42 + 43 + Object.entries(body).forEach(([key, value]) => { 44 + if (value === undefined || value === null) { 45 + return; 46 + } 47 + if (Array.isArray(value)) { 48 + value.forEach((v) => serializeFormDataPair(data, key, v)); 49 + } else { 50 + serializeFormDataPair(data, key, value); 51 + } 52 + }); 53 + 54 + return data; 55 + }, 56 + }; 57 + 58 + export const jsonBodySerializer = { 59 + bodySerializer: <T>(body: T) => 60 + JSON.stringify(body, (key, value) => 61 + typeof value === 'bigint' ? value.toString() : value, 62 + ), 63 + }; 64 + 65 + export const urlSearchParamsBodySerializer = { 66 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 67 + body: T, 68 + ) => { 69 + const data = new URLSearchParams(); 70 + 71 + Object.entries(body).forEach(([key, value]) => { 72 + if (value === undefined || value === null) { 73 + return; 74 + } 75 + if (Array.isArray(value)) { 76 + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); 77 + } else { 78 + serializeUrlSearchParamsPair(data, key, value); 79 + } 80 + }); 81 + 82 + return data.toString(); 83 + }, 84 + };
+141
examples/openapi-ts-tanstack-react-query/src/client/core/params.ts
··· 1 + type Slot = 'body' | 'headers' | 'path' | 'query'; 2 + 3 + export type Field = 4 + | { 5 + in: Exclude<Slot, 'body'>; 6 + key: string; 7 + map?: string; 8 + } 9 + | { 10 + in: Extract<Slot, 'body'>; 11 + key?: string; 12 + map?: string; 13 + }; 14 + 15 + export interface Fields { 16 + allowExtra?: Partial<Record<Slot, boolean>>; 17 + args?: ReadonlyArray<Field>; 18 + } 19 + 20 + export type FieldsConfig = ReadonlyArray<Field | Fields>; 21 + 22 + const extraPrefixesMap: Record<string, Slot> = { 23 + $body_: 'body', 24 + $headers_: 'headers', 25 + $path_: 'path', 26 + $query_: 'query', 27 + }; 28 + const extraPrefixes = Object.entries(extraPrefixesMap); 29 + 30 + type KeyMap = Map< 31 + string, 32 + { 33 + in: Slot; 34 + map?: string; 35 + } 36 + >; 37 + 38 + const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { 39 + if (!map) { 40 + map = new Map(); 41 + } 42 + 43 + for (const config of fields) { 44 + if ('in' in config) { 45 + if (config.key) { 46 + map.set(config.key, { 47 + in: config.in, 48 + map: config.map, 49 + }); 50 + } 51 + } else if (config.args) { 52 + buildKeyMap(config.args, map); 53 + } 54 + } 55 + 56 + return map; 57 + }; 58 + 59 + interface Params { 60 + body: unknown; 61 + headers: Record<string, unknown>; 62 + path: Record<string, unknown>; 63 + query: Record<string, unknown>; 64 + } 65 + 66 + const stripEmptySlots = (params: Params) => { 67 + for (const [slot, value] of Object.entries(params)) { 68 + if (value && typeof value === 'object' && !Object.keys(value).length) { 69 + delete params[slot as Slot]; 70 + } 71 + } 72 + }; 73 + 74 + export const buildClientParams = ( 75 + args: ReadonlyArray<unknown>, 76 + fields: FieldsConfig, 77 + ) => { 78 + const params: Params = { 79 + body: {}, 80 + headers: {}, 81 + path: {}, 82 + query: {}, 83 + }; 84 + 85 + const map = buildKeyMap(fields); 86 + 87 + let config: FieldsConfig[number] | undefined; 88 + 89 + for (const [index, arg] of args.entries()) { 90 + if (fields[index]) { 91 + config = fields[index]; 92 + } 93 + 94 + if (!config) { 95 + continue; 96 + } 97 + 98 + if ('in' in config) { 99 + if (config.key) { 100 + const field = map.get(config.key)!; 101 + const name = field.map || config.key; 102 + (params[field.in] as Record<string, unknown>)[name] = arg; 103 + } else { 104 + params.body = arg; 105 + } 106 + } else { 107 + for (const [key, value] of Object.entries(arg ?? {})) { 108 + const field = map.get(key); 109 + 110 + if (field) { 111 + const name = field.map || key; 112 + (params[field.in] as Record<string, unknown>)[name] = value; 113 + } else { 114 + const extra = extraPrefixes.find(([prefix]) => 115 + key.startsWith(prefix), 116 + ); 117 + 118 + if (extra) { 119 + const [prefix, slot] = extra; 120 + (params[slot] as Record<string, unknown>)[ 121 + key.slice(prefix.length) 122 + ] = value; 123 + } else { 124 + for (const [slot, allowed] of Object.entries( 125 + config.allowExtra ?? {}, 126 + )) { 127 + if (allowed) { 128 + (params[slot as Slot] as Record<string, unknown>)[key] = value; 129 + break; 130 + } 131 + } 132 + } 133 + } 134 + } 135 + } 136 + } 137 + 138 + stripEmptySlots(params); 139 + 140 + return params; 141 + };
+179
examples/openapi-ts-tanstack-react-query/src/client/core/pathSerializer.ts
··· 1 + interface SerializeOptions<T> 2 + extends SerializePrimitiveOptions, 3 + SerializerOptions<T> {} 4 + 5 + interface SerializePrimitiveOptions { 6 + allowReserved?: boolean; 7 + name: string; 8 + } 9 + 10 + export interface SerializerOptions<T> { 11 + /** 12 + * @default true 13 + */ 14 + explode: boolean; 15 + style: T; 16 + } 17 + 18 + export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; 19 + export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 20 + type MatrixStyle = 'label' | 'matrix' | 'simple'; 21 + export type ObjectStyle = 'form' | 'deepObject'; 22 + type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; 23 + 24 + interface SerializePrimitiveParam extends SerializePrimitiveOptions { 25 + value: string; 26 + } 27 + 28 + export const separatorArrayExplode = (style: ArraySeparatorStyle) => { 29 + switch (style) { 30 + case 'label': 31 + return '.'; 32 + case 'matrix': 33 + return ';'; 34 + case 'simple': 35 + return ','; 36 + default: 37 + return '&'; 38 + } 39 + }; 40 + 41 + export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { 42 + switch (style) { 43 + case 'form': 44 + return ','; 45 + case 'pipeDelimited': 46 + return '|'; 47 + case 'spaceDelimited': 48 + return '%20'; 49 + default: 50 + return ','; 51 + } 52 + }; 53 + 54 + export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { 55 + switch (style) { 56 + case 'label': 57 + return '.'; 58 + case 'matrix': 59 + return ';'; 60 + case 'simple': 61 + return ','; 62 + default: 63 + return '&'; 64 + } 65 + }; 66 + 67 + export const serializeArrayParam = ({ 68 + allowReserved, 69 + explode, 70 + name, 71 + style, 72 + value, 73 + }: SerializeOptions<ArraySeparatorStyle> & { 74 + value: unknown[]; 75 + }) => { 76 + if (!explode) { 77 + const joinedValues = ( 78 + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) 79 + ).join(separatorArrayNoExplode(style)); 80 + switch (style) { 81 + case 'label': 82 + return `.${joinedValues}`; 83 + case 'matrix': 84 + return `;${name}=${joinedValues}`; 85 + case 'simple': 86 + return joinedValues; 87 + default: 88 + return `${name}=${joinedValues}`; 89 + } 90 + } 91 + 92 + const separator = separatorArrayExplode(style); 93 + const joinedValues = value 94 + .map((v) => { 95 + if (style === 'label' || style === 'simple') { 96 + return allowReserved ? v : encodeURIComponent(v as string); 97 + } 98 + 99 + return serializePrimitiveParam({ 100 + allowReserved, 101 + name, 102 + value: v as string, 103 + }); 104 + }) 105 + .join(separator); 106 + return style === 'label' || style === 'matrix' 107 + ? separator + joinedValues 108 + : joinedValues; 109 + }; 110 + 111 + export const serializePrimitiveParam = ({ 112 + allowReserved, 113 + name, 114 + value, 115 + }: SerializePrimitiveParam) => { 116 + if (value === undefined || value === null) { 117 + return ''; 118 + } 119 + 120 + if (typeof value === 'object') { 121 + throw new Error( 122 + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', 123 + ); 124 + } 125 + 126 + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; 127 + }; 128 + 129 + export const serializeObjectParam = ({ 130 + allowReserved, 131 + explode, 132 + name, 133 + style, 134 + value, 135 + valueOnly, 136 + }: SerializeOptions<ObjectSeparatorStyle> & { 137 + value: Record<string, unknown> | Date; 138 + valueOnly?: boolean; 139 + }) => { 140 + if (value instanceof Date) { 141 + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; 142 + } 143 + 144 + if (style !== 'deepObject' && !explode) { 145 + let values: string[] = []; 146 + Object.entries(value).forEach(([key, v]) => { 147 + values = [ 148 + ...values, 149 + key, 150 + allowReserved ? (v as string) : encodeURIComponent(v as string), 151 + ]; 152 + }); 153 + const joinedValues = values.join(','); 154 + switch (style) { 155 + case 'form': 156 + return `${name}=${joinedValues}`; 157 + case 'label': 158 + return `.${joinedValues}`; 159 + case 'matrix': 160 + return `;${name}=${joinedValues}`; 161 + default: 162 + return joinedValues; 163 + } 164 + } 165 + 166 + const separator = separatorObjectExplode(style); 167 + const joinedValues = Object.entries(value) 168 + .map(([key, v]) => 169 + serializePrimitiveParam({ 170 + allowReserved, 171 + name: style === 'deepObject' ? `${name}[${key}]` : key, 172 + value: v as string, 173 + }), 174 + ) 175 + .join(separator); 176 + return style === 'label' || style === 'matrix' 177 + ? separator + joinedValues 178 + : joinedValues; 179 + };
+98
examples/openapi-ts-tanstack-react-query/src/client/core/types.ts
··· 1 + import type { Auth, AuthToken } from './auth'; 2 + import type { 3 + BodySerializer, 4 + QuerySerializer, 5 + QuerySerializerOptions, 6 + } from './bodySerializer'; 7 + 8 + export interface Client< 9 + RequestFn = never, 10 + Config = unknown, 11 + MethodFn = never, 12 + BuildUrlFn = never, 13 + > { 14 + /** 15 + * Returns the final request URL. 16 + */ 17 + buildUrl: BuildUrlFn; 18 + connect: MethodFn; 19 + delete: MethodFn; 20 + get: MethodFn; 21 + getConfig: () => Config; 22 + head: MethodFn; 23 + options: MethodFn; 24 + patch: MethodFn; 25 + post: MethodFn; 26 + put: MethodFn; 27 + request: RequestFn; 28 + setConfig: (config: Config) => Config; 29 + trace: MethodFn; 30 + } 31 + 32 + export interface Config { 33 + /** 34 + * Auth token or a function returning auth token. The resolved value will be 35 + * added to the request payload as defined by its `security` array. 36 + */ 37 + auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken; 38 + /** 39 + * A function for serializing request body parameter. By default, 40 + * {@link JSON.stringify()} will be used. 41 + */ 42 + bodySerializer?: BodySerializer | null; 43 + /** 44 + * An object containing any HTTP headers that you want to pre-populate your 45 + * `Headers` object with. 46 + * 47 + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} 48 + */ 49 + headers?: 50 + | RequestInit['headers'] 51 + | Record< 52 + string, 53 + | string 54 + | number 55 + | boolean 56 + | (string | number | boolean)[] 57 + | null 58 + | undefined 59 + | unknown 60 + >; 61 + /** 62 + * The request method. 63 + * 64 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} 65 + */ 66 + method?: 67 + | 'CONNECT' 68 + | 'DELETE' 69 + | 'GET' 70 + | 'HEAD' 71 + | 'OPTIONS' 72 + | 'PATCH' 73 + | 'POST' 74 + | 'PUT' 75 + | 'TRACE'; 76 + /** 77 + * A function for serializing request query parameters. By default, arrays 78 + * will be exploded in form style, objects will be exploded in deepObject 79 + * style, and reserved characters are percent-encoded. 80 + * 81 + * This method will have no effect if the native `paramsSerializer()` Axios 82 + * API function is used. 83 + * 84 + * {@link https://swagger.io/docs/specification/serialization/#query View examples} 85 + */ 86 + querySerializer?: QuerySerializer | QuerySerializerOptions; 87 + /** 88 + * A function transforming response data before it's returned. This is useful 89 + * for post-processing data, e.g. converting ISO strings into Date objects. 90 + */ 91 + responseTransformer?: (data: unknown) => Promise<unknown>; 92 + /** 93 + * A function validating response data. This is useful if you want to ensure 94 + * the response conforms to the desired shape, so it can be safely passed to 95 + * the transformers and returned to the user. 96 + */ 97 + responseValidator?: (data: unknown) => Promise<unknown>; 98 + }
+5 -10
examples/openapi-ts-tanstack-react-query/src/client/sdk.gen.ts
··· 1 1 // This file is auto-generated by @hey-api/openapi-ts 2 2 3 - import type { 4 - Client, 5 - Options as ClientOptions, 6 - TDataShape, 7 - } from '@hey-api/client-fetch'; 8 - 3 + import type { Client, Options as ClientOptions, TDataShape } from './client'; 9 4 import { client as _heyApiClient } from './client.gen'; 10 5 import type { 11 6 AddPetData, ··· 106 101 ...options, 107 102 headers: { 108 103 'Content-Type': 'application/json', 109 - ...options?.headers, 104 + ...options.headers, 110 105 }, 111 106 }); 112 107 ··· 132 127 ...options, 133 128 headers: { 134 129 'Content-Type': 'application/json', 135 - ...options?.headers, 130 + ...options.headers, 136 131 }, 137 132 }); 138 133 ··· 273 268 ...options, 274 269 headers: { 275 270 'Content-Type': 'application/octet-stream', 276 - ...options?.headers, 271 + ...options.headers, 277 272 }, 278 273 }); 279 274 ··· 471 466 ...options, 472 467 headers: { 473 468 'Content-Type': 'application/json', 474 - ...options?.headers, 469 + ...options.headers, 475 470 }, 476 471 });
-1
examples/openapi-ts-tanstack-svelte-query/package.json
··· 16 16 "test": "vitest --watch=false" 17 17 }, 18 18 "dependencies": { 19 - "@hey-api/client-fetch": "workspace:*", 20 19 "@tanstack/svelte-query": "5.73.3" 21 20 }, 22 21 "devDependencies": {
+1 -2
examples/openapi-ts-tanstack-svelte-query/src/client/client.gen.ts
··· 5 5 type Config, 6 6 createClient, 7 7 createConfig, 8 - } from '@hey-api/client-fetch'; 9 - 8 + } from './client'; 10 9 import type { ClientOptions } from './types.gen'; 11 10 12 11 /**
+181
examples/openapi-ts-tanstack-svelte-query/src/client/client/client.ts
··· 1 + import type { Client, Config, RequestOptions } from './types'; 2 + import { 3 + buildUrl, 4 + createConfig, 5 + createInterceptors, 6 + getParseAs, 7 + mergeConfigs, 8 + mergeHeaders, 9 + setAuthParams, 10 + } from './utils'; 11 + 12 + type ReqInit = Omit<RequestInit, 'body' | 'headers'> & { 13 + body?: any; 14 + headers: ReturnType<typeof mergeHeaders>; 15 + }; 16 + 17 + export const createClient = (config: Config = {}): Client => { 18 + let _config = mergeConfigs(createConfig(), config); 19 + 20 + const getConfig = (): Config => ({ ..._config }); 21 + 22 + const setConfig = (config: Config): Config => { 23 + _config = mergeConfigs(_config, config); 24 + return getConfig(); 25 + }; 26 + 27 + const interceptors = createInterceptors< 28 + Request, 29 + Response, 30 + unknown, 31 + RequestOptions 32 + >(); 33 + 34 + const request: Client['request'] = async (options) => { 35 + const opts = { 36 + ..._config, 37 + ...options, 38 + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, 39 + headers: mergeHeaders(_config.headers, options.headers), 40 + }; 41 + 42 + if (opts.security) { 43 + await setAuthParams({ 44 + ...opts, 45 + security: opts.security, 46 + }); 47 + } 48 + 49 + if (opts.body && opts.bodySerializer) { 50 + opts.body = opts.bodySerializer(opts.body); 51 + } 52 + 53 + // remove Content-Type header if body is empty to avoid sending invalid requests 54 + if (opts.body === undefined || opts.body === '') { 55 + opts.headers.delete('Content-Type'); 56 + } 57 + 58 + const url = buildUrl(opts); 59 + const requestInit: ReqInit = { 60 + redirect: 'follow', 61 + ...opts, 62 + }; 63 + 64 + let request = new Request(url, requestInit); 65 + 66 + for (const fn of interceptors.request._fns) { 67 + if (fn) { 68 + request = await fn(request, opts); 69 + } 70 + } 71 + 72 + // fetch must be assigned here, otherwise it would throw the error: 73 + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation 74 + const _fetch = opts.fetch!; 75 + let response = await _fetch(request); 76 + 77 + for (const fn of interceptors.response._fns) { 78 + if (fn) { 79 + response = await fn(response, request, opts); 80 + } 81 + } 82 + 83 + const result = { 84 + request, 85 + response, 86 + }; 87 + 88 + if (response.ok) { 89 + if ( 90 + response.status === 204 || 91 + response.headers.get('Content-Length') === '0' 92 + ) { 93 + return opts.responseStyle === 'data' 94 + ? {} 95 + : { 96 + data: {}, 97 + ...result, 98 + }; 99 + } 100 + 101 + const parseAs = 102 + (opts.parseAs === 'auto' 103 + ? getParseAs(response.headers.get('Content-Type')) 104 + : opts.parseAs) ?? 'json'; 105 + 106 + if (parseAs === 'stream') { 107 + return opts.responseStyle === 'data' 108 + ? response.body 109 + : { 110 + data: response.body, 111 + ...result, 112 + }; 113 + } 114 + 115 + let data = await response[parseAs](); 116 + if (parseAs === 'json') { 117 + if (opts.responseValidator) { 118 + await opts.responseValidator(data); 119 + } 120 + 121 + if (opts.responseTransformer) { 122 + data = await opts.responseTransformer(data); 123 + } 124 + } 125 + 126 + return opts.responseStyle === 'data' 127 + ? data 128 + : { 129 + data, 130 + ...result, 131 + }; 132 + } 133 + 134 + let error = await response.text(); 135 + 136 + try { 137 + error = JSON.parse(error); 138 + } catch { 139 + // noop 140 + } 141 + 142 + let finalError = error; 143 + 144 + for (const fn of interceptors.error._fns) { 145 + if (fn) { 146 + finalError = (await fn(error, response, request, opts)) as string; 147 + } 148 + } 149 + 150 + finalError = finalError || ({} as string); 151 + 152 + if (opts.throwOnError) { 153 + throw finalError; 154 + } 155 + 156 + // TODO: we probably want to return error and improve types 157 + return opts.responseStyle === 'data' 158 + ? undefined 159 + : { 160 + error: finalError, 161 + ...result, 162 + }; 163 + }; 164 + 165 + return { 166 + buildUrl, 167 + connect: (options) => request({ ...options, method: 'CONNECT' }), 168 + delete: (options) => request({ ...options, method: 'DELETE' }), 169 + get: (options) => request({ ...options, method: 'GET' }), 170 + getConfig, 171 + head: (options) => request({ ...options, method: 'HEAD' }), 172 + interceptors, 173 + options: (options) => request({ ...options, method: 'OPTIONS' }), 174 + patch: (options) => request({ ...options, method: 'PATCH' }), 175 + post: (options) => request({ ...options, method: 'POST' }), 176 + put: (options) => request({ ...options, method: 'PUT' }), 177 + request, 178 + setConfig, 179 + trace: (options) => request({ ...options, method: 'TRACE' }), 180 + }; 181 + };
+22
examples/openapi-ts-tanstack-svelte-query/src/client/client/index.ts
··· 1 + export type { Auth } from '../core/auth'; 2 + export type { QuerySerializerOptions } from '../core/bodySerializer'; 3 + export { 4 + formDataBodySerializer, 5 + jsonBodySerializer, 6 + urlSearchParamsBodySerializer, 7 + } from '../core/bodySerializer'; 8 + export { buildClientParams } from '../core/params'; 9 + export { createClient } from './client'; 10 + export type { 11 + Client, 12 + ClientOptions, 13 + Config, 14 + CreateClientConfig, 15 + Options, 16 + OptionsLegacyParser, 17 + RequestOptions, 18 + RequestResult, 19 + ResponseStyle, 20 + TDataShape, 21 + } from './types'; 22 + export { createConfig, mergeHeaders } from './utils';
+212
examples/openapi-ts-tanstack-svelte-query/src/client/client/types.ts
··· 1 + import type { Auth } from '../core/auth'; 2 + import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; 3 + import type { Middleware } from './utils'; 4 + 5 + export type ResponseStyle = 'data' | 'fields'; 6 + 7 + export interface Config<T extends ClientOptions = ClientOptions> 8 + extends Omit<RequestInit, 'body' | 'headers' | 'method'>, 9 + CoreConfig { 10 + /** 11 + * Base URL for all requests made by this client. 12 + */ 13 + baseUrl?: T['baseUrl']; 14 + /** 15 + * Fetch API implementation. You can use this option to provide a custom 16 + * fetch instance. 17 + * 18 + * @default globalThis.fetch 19 + */ 20 + fetch?: (request: Request) => ReturnType<typeof fetch>; 21 + /** 22 + * Please don't use the Fetch client for Next.js applications. The `next` 23 + * options won't have any effect. 24 + * 25 + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. 26 + */ 27 + next?: never; 28 + /** 29 + * Return the response data parsed in a specified format. By default, `auto` 30 + * will infer the appropriate method from the `Content-Type` response header. 31 + * You can override this behavior with any of the {@link Body} methods. 32 + * Select `stream` if you don't want to parse response data at all. 33 + * 34 + * @default 'auto' 35 + */ 36 + parseAs?: Exclude<keyof Body, 'body' | 'bodyUsed'> | 'auto' | 'stream'; 37 + /** 38 + * Should we return only data or multiple fields (data, error, response, etc.)? 39 + * 40 + * @default 'fields' 41 + */ 42 + responseStyle?: ResponseStyle; 43 + /** 44 + * Throw an error instead of returning it in the response? 45 + * 46 + * @default false 47 + */ 48 + throwOnError?: T['throwOnError']; 49 + } 50 + 51 + export interface RequestOptions< 52 + TResponseStyle extends ResponseStyle = 'fields', 53 + ThrowOnError extends boolean = boolean, 54 + Url extends string = string, 55 + > extends Config<{ 56 + responseStyle: TResponseStyle; 57 + throwOnError: ThrowOnError; 58 + }> { 59 + /** 60 + * Any body that you want to add to your request. 61 + * 62 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} 63 + */ 64 + body?: unknown; 65 + path?: Record<string, unknown>; 66 + query?: Record<string, unknown>; 67 + /** 68 + * Security mechanism(s) to use for the request. 69 + */ 70 + security?: ReadonlyArray<Auth>; 71 + url: Url; 72 + } 73 + 74 + export type RequestResult< 75 + TData = unknown, 76 + TError = unknown, 77 + ThrowOnError extends boolean = boolean, 78 + TResponseStyle extends ResponseStyle = 'fields', 79 + > = ThrowOnError extends true 80 + ? Promise< 81 + TResponseStyle extends 'data' 82 + ? TData extends Record<string, unknown> 83 + ? TData[keyof TData] 84 + : TData 85 + : { 86 + data: TData extends Record<string, unknown> 87 + ? TData[keyof TData] 88 + : TData; 89 + request: Request; 90 + response: Response; 91 + } 92 + > 93 + : Promise< 94 + TResponseStyle extends 'data' 95 + ? 96 + | (TData extends Record<string, unknown> 97 + ? TData[keyof TData] 98 + : TData) 99 + | undefined 100 + : ( 101 + | { 102 + data: TData extends Record<string, unknown> 103 + ? TData[keyof TData] 104 + : TData; 105 + error: undefined; 106 + } 107 + | { 108 + data: undefined; 109 + error: TError extends Record<string, unknown> 110 + ? TError[keyof TError] 111 + : TError; 112 + } 113 + ) & { 114 + request: Request; 115 + response: Response; 116 + } 117 + >; 118 + 119 + export interface ClientOptions { 120 + baseUrl?: string; 121 + responseStyle?: ResponseStyle; 122 + throwOnError?: boolean; 123 + } 124 + 125 + type MethodFn = < 126 + TData = unknown, 127 + TError = unknown, 128 + ThrowOnError extends boolean = false, 129 + TResponseStyle extends ResponseStyle = 'fields', 130 + >( 131 + options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'>, 132 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 133 + 134 + type RequestFn = < 135 + TData = unknown, 136 + TError = unknown, 137 + ThrowOnError extends boolean = false, 138 + TResponseStyle extends ResponseStyle = 'fields', 139 + >( 140 + options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'> & 141 + Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, 'method'>, 142 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 143 + 144 + type BuildUrlFn = < 145 + TData extends { 146 + body?: unknown; 147 + path?: Record<string, unknown>; 148 + query?: Record<string, unknown>; 149 + url: string; 150 + }, 151 + >( 152 + options: Pick<TData, 'url'> & Options<TData>, 153 + ) => string; 154 + 155 + export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & { 156 + interceptors: Middleware<Request, Response, unknown, RequestOptions>; 157 + }; 158 + 159 + /** 160 + * The `createClientConfig()` function will be called on client initialization 161 + * and the returned object will become the client's initial configuration. 162 + * 163 + * You may want to initialize your client this way instead of calling 164 + * `setConfig()`. This is useful for example if you're using Next.js 165 + * to ensure your client always has the correct values. 166 + */ 167 + export type CreateClientConfig<T extends ClientOptions = ClientOptions> = ( 168 + override?: Config<ClientOptions & T>, 169 + ) => Config<Required<ClientOptions> & T>; 170 + 171 + export interface TDataShape { 172 + body?: unknown; 173 + headers?: unknown; 174 + path?: unknown; 175 + query?: unknown; 176 + url: string; 177 + } 178 + 179 + type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>; 180 + 181 + export type Options< 182 + TData extends TDataShape = TDataShape, 183 + ThrowOnError extends boolean = boolean, 184 + TResponseStyle extends ResponseStyle = 'fields', 185 + > = OmitKeys< 186 + RequestOptions<TResponseStyle, ThrowOnError>, 187 + 'body' | 'path' | 'query' | 'url' 188 + > & 189 + Omit<TData, 'url'>; 190 + 191 + export type OptionsLegacyParser< 192 + TData = unknown, 193 + ThrowOnError extends boolean = boolean, 194 + TResponseStyle extends ResponseStyle = 'fields', 195 + > = TData extends { body?: any } 196 + ? TData extends { headers?: any } 197 + ? OmitKeys< 198 + RequestOptions<TResponseStyle, ThrowOnError>, 199 + 'body' | 'headers' | 'url' 200 + > & 201 + TData 202 + : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'body' | 'url'> & 203 + TData & 204 + Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'headers'> 205 + : TData extends { headers?: any } 206 + ? OmitKeys< 207 + RequestOptions<TResponseStyle, ThrowOnError>, 208 + 'headers' | 'url' 209 + > & 210 + TData & 211 + Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'body'> 212 + : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'url'> & TData;
+415
examples/openapi-ts-tanstack-svelte-query/src/client/client/utils.ts
··· 1 + import { getAuthToken } from '../core/auth'; 2 + import type { 3 + QuerySerializer, 4 + QuerySerializerOptions, 5 + } from '../core/bodySerializer'; 6 + import { jsonBodySerializer } from '../core/bodySerializer'; 7 + import { 8 + serializeArrayParam, 9 + serializeObjectParam, 10 + serializePrimitiveParam, 11 + } from '../core/pathSerializer'; 12 + import type { Client, ClientOptions, Config, RequestOptions } from './types'; 13 + 14 + interface PathSerializer { 15 + path: Record<string, unknown>; 16 + url: string; 17 + } 18 + 19 + const PATH_PARAM_RE = /\{[^{}]+\}/g; 20 + 21 + type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; 22 + type MatrixStyle = 'label' | 'matrix' | 'simple'; 23 + type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 24 + 25 + const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { 26 + let url = _url; 27 + const matches = _url.match(PATH_PARAM_RE); 28 + if (matches) { 29 + for (const match of matches) { 30 + let explode = false; 31 + let name = match.substring(1, match.length - 1); 32 + let style: ArraySeparatorStyle = 'simple'; 33 + 34 + if (name.endsWith('*')) { 35 + explode = true; 36 + name = name.substring(0, name.length - 1); 37 + } 38 + 39 + if (name.startsWith('.')) { 40 + name = name.substring(1); 41 + style = 'label'; 42 + } else if (name.startsWith(';')) { 43 + name = name.substring(1); 44 + style = 'matrix'; 45 + } 46 + 47 + const value = path[name]; 48 + 49 + if (value === undefined || value === null) { 50 + continue; 51 + } 52 + 53 + if (Array.isArray(value)) { 54 + url = url.replace( 55 + match, 56 + serializeArrayParam({ explode, name, style, value }), 57 + ); 58 + continue; 59 + } 60 + 61 + if (typeof value === 'object') { 62 + url = url.replace( 63 + match, 64 + serializeObjectParam({ 65 + explode, 66 + name, 67 + style, 68 + value: value as Record<string, unknown>, 69 + valueOnly: true, 70 + }), 71 + ); 72 + continue; 73 + } 74 + 75 + if (style === 'matrix') { 76 + url = url.replace( 77 + match, 78 + `;${serializePrimitiveParam({ 79 + name, 80 + value: value as string, 81 + })}`, 82 + ); 83 + continue; 84 + } 85 + 86 + const replaceValue = encodeURIComponent( 87 + style === 'label' ? `.${value as string}` : (value as string), 88 + ); 89 + url = url.replace(match, replaceValue); 90 + } 91 + } 92 + return url; 93 + }; 94 + 95 + export const createQuerySerializer = <T = unknown>({ 96 + allowReserved, 97 + array, 98 + object, 99 + }: QuerySerializerOptions = {}) => { 100 + const querySerializer = (queryParams: T) => { 101 + const search: string[] = []; 102 + if (queryParams && typeof queryParams === 'object') { 103 + for (const name in queryParams) { 104 + const value = queryParams[name]; 105 + 106 + if (value === undefined || value === null) { 107 + continue; 108 + } 109 + 110 + if (Array.isArray(value)) { 111 + const serializedArray = serializeArrayParam({ 112 + allowReserved, 113 + explode: true, 114 + name, 115 + style: 'form', 116 + value, 117 + ...array, 118 + }); 119 + if (serializedArray) search.push(serializedArray); 120 + } else if (typeof value === 'object') { 121 + const serializedObject = serializeObjectParam({ 122 + allowReserved, 123 + explode: true, 124 + name, 125 + style: 'deepObject', 126 + value: value as Record<string, unknown>, 127 + ...object, 128 + }); 129 + if (serializedObject) search.push(serializedObject); 130 + } else { 131 + const serializedPrimitive = serializePrimitiveParam({ 132 + allowReserved, 133 + name, 134 + value: value as string, 135 + }); 136 + if (serializedPrimitive) search.push(serializedPrimitive); 137 + } 138 + } 139 + } 140 + return search.join('&'); 141 + }; 142 + return querySerializer; 143 + }; 144 + 145 + /** 146 + * Infers parseAs value from provided Content-Type header. 147 + */ 148 + export const getParseAs = ( 149 + contentType: string | null, 150 + ): Exclude<Config['parseAs'], 'auto'> => { 151 + if (!contentType) { 152 + // If no Content-Type header is provided, the best we can do is return the raw response body, 153 + // which is effectively the same as the 'stream' option. 154 + return 'stream'; 155 + } 156 + 157 + const cleanContent = contentType.split(';')[0]?.trim(); 158 + 159 + if (!cleanContent) { 160 + return; 161 + } 162 + 163 + if ( 164 + cleanContent.startsWith('application/json') || 165 + cleanContent.endsWith('+json') 166 + ) { 167 + return 'json'; 168 + } 169 + 170 + if (cleanContent === 'multipart/form-data') { 171 + return 'formData'; 172 + } 173 + 174 + if ( 175 + ['application/', 'audio/', 'image/', 'video/'].some((type) => 176 + cleanContent.startsWith(type), 177 + ) 178 + ) { 179 + return 'blob'; 180 + } 181 + 182 + if (cleanContent.startsWith('text/')) { 183 + return 'text'; 184 + } 185 + }; 186 + 187 + export const setAuthParams = async ({ 188 + security, 189 + ...options 190 + }: Pick<Required<RequestOptions>, 'security'> & 191 + Pick<RequestOptions, 'auth' | 'query'> & { 192 + headers: Headers; 193 + }) => { 194 + for (const auth of security) { 195 + const token = await getAuthToken(auth, options.auth); 196 + 197 + if (!token) { 198 + continue; 199 + } 200 + 201 + const name = auth.name ?? 'Authorization'; 202 + 203 + switch (auth.in) { 204 + case 'query': 205 + if (!options.query) { 206 + options.query = {}; 207 + } 208 + options.query[name] = token; 209 + break; 210 + case 'cookie': 211 + options.headers.append('Cookie', `${name}=${token}`); 212 + break; 213 + case 'header': 214 + default: 215 + options.headers.set(name, token); 216 + break; 217 + } 218 + 219 + return; 220 + } 221 + }; 222 + 223 + export const buildUrl: Client['buildUrl'] = (options) => { 224 + const url = getUrl({ 225 + baseUrl: options.baseUrl as string, 226 + path: options.path, 227 + query: options.query, 228 + querySerializer: 229 + typeof options.querySerializer === 'function' 230 + ? options.querySerializer 231 + : createQuerySerializer(options.querySerializer), 232 + url: options.url, 233 + }); 234 + return url; 235 + }; 236 + 237 + export const getUrl = ({ 238 + baseUrl, 239 + path, 240 + query, 241 + querySerializer, 242 + url: _url, 243 + }: { 244 + baseUrl?: string; 245 + path?: Record<string, unknown>; 246 + query?: Record<string, unknown>; 247 + querySerializer: QuerySerializer; 248 + url: string; 249 + }) => { 250 + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; 251 + let url = (baseUrl ?? '') + pathUrl; 252 + if (path) { 253 + url = defaultPathSerializer({ path, url }); 254 + } 255 + let search = query ? querySerializer(query) : ''; 256 + if (search.startsWith('?')) { 257 + search = search.substring(1); 258 + } 259 + if (search) { 260 + url += `?${search}`; 261 + } 262 + return url; 263 + }; 264 + 265 + export const mergeConfigs = (a: Config, b: Config): Config => { 266 + const config = { ...a, ...b }; 267 + if (config.baseUrl?.endsWith('/')) { 268 + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); 269 + } 270 + config.headers = mergeHeaders(a.headers, b.headers); 271 + return config; 272 + }; 273 + 274 + export const mergeHeaders = ( 275 + ...headers: Array<Required<Config>['headers'] | undefined> 276 + ): Headers => { 277 + const mergedHeaders = new Headers(); 278 + for (const header of headers) { 279 + if (!header || typeof header !== 'object') { 280 + continue; 281 + } 282 + 283 + const iterator = 284 + header instanceof Headers ? header.entries() : Object.entries(header); 285 + 286 + for (const [key, value] of iterator) { 287 + if (value === null) { 288 + mergedHeaders.delete(key); 289 + } else if (Array.isArray(value)) { 290 + for (const v of value) { 291 + mergedHeaders.append(key, v as string); 292 + } 293 + } else if (value !== undefined) { 294 + // assume object headers are meant to be JSON stringified, i.e. their 295 + // content value in OpenAPI specification is 'application/json' 296 + mergedHeaders.set( 297 + key, 298 + typeof value === 'object' ? JSON.stringify(value) : (value as string), 299 + ); 300 + } 301 + } 302 + } 303 + return mergedHeaders; 304 + }; 305 + 306 + type ErrInterceptor<Err, Res, Req, Options> = ( 307 + error: Err, 308 + response: Res, 309 + request: Req, 310 + options: Options, 311 + ) => Err | Promise<Err>; 312 + 313 + type ReqInterceptor<Req, Options> = ( 314 + request: Req, 315 + options: Options, 316 + ) => Req | Promise<Req>; 317 + 318 + type ResInterceptor<Res, Req, Options> = ( 319 + response: Res, 320 + request: Req, 321 + options: Options, 322 + ) => Res | Promise<Res>; 323 + 324 + class Interceptors<Interceptor> { 325 + _fns: (Interceptor | null)[]; 326 + 327 + constructor() { 328 + this._fns = []; 329 + } 330 + 331 + clear() { 332 + this._fns = []; 333 + } 334 + 335 + getInterceptorIndex(id: number | Interceptor): number { 336 + if (typeof id === 'number') { 337 + return this._fns[id] ? id : -1; 338 + } else { 339 + return this._fns.indexOf(id); 340 + } 341 + } 342 + exists(id: number | Interceptor) { 343 + const index = this.getInterceptorIndex(id); 344 + return !!this._fns[index]; 345 + } 346 + 347 + eject(id: number | Interceptor) { 348 + const index = this.getInterceptorIndex(id); 349 + if (this._fns[index]) { 350 + this._fns[index] = null; 351 + } 352 + } 353 + 354 + update(id: number | Interceptor, fn: Interceptor) { 355 + const index = this.getInterceptorIndex(id); 356 + if (this._fns[index]) { 357 + this._fns[index] = fn; 358 + return id; 359 + } else { 360 + return false; 361 + } 362 + } 363 + 364 + use(fn: Interceptor) { 365 + this._fns = [...this._fns, fn]; 366 + return this._fns.length - 1; 367 + } 368 + } 369 + 370 + // `createInterceptors()` response, meant for external use as it does not 371 + // expose internals 372 + export interface Middleware<Req, Res, Err, Options> { 373 + error: Pick< 374 + Interceptors<ErrInterceptor<Err, Res, Req, Options>>, 375 + 'eject' | 'use' 376 + >; 377 + request: Pick<Interceptors<ReqInterceptor<Req, Options>>, 'eject' | 'use'>; 378 + response: Pick< 379 + Interceptors<ResInterceptor<Res, Req, Options>>, 380 + 'eject' | 'use' 381 + >; 382 + } 383 + 384 + // do not add `Middleware` as return type so we can use _fns internally 385 + export const createInterceptors = <Req, Res, Err, Options>() => ({ 386 + error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(), 387 + request: new Interceptors<ReqInterceptor<Req, Options>>(), 388 + response: new Interceptors<ResInterceptor<Res, Req, Options>>(), 389 + }); 390 + 391 + const defaultQuerySerializer = createQuerySerializer({ 392 + allowReserved: false, 393 + array: { 394 + explode: true, 395 + style: 'form', 396 + }, 397 + object: { 398 + explode: true, 399 + style: 'deepObject', 400 + }, 401 + }); 402 + 403 + const defaultHeaders = { 404 + 'Content-Type': 'application/json', 405 + }; 406 + 407 + export const createConfig = <T extends ClientOptions = ClientOptions>( 408 + override: Config<Omit<ClientOptions, keyof T> & T> = {}, 409 + ): Config<Omit<ClientOptions, keyof T> & T> => ({ 410 + ...jsonBodySerializer, 411 + headers: defaultHeaders, 412 + parseAs: 'auto', 413 + querySerializer: defaultQuerySerializer, 414 + ...override, 415 + });
+40
examples/openapi-ts-tanstack-svelte-query/src/client/core/auth.ts
··· 1 + export type AuthToken = string | undefined; 2 + 3 + export interface Auth { 4 + /** 5 + * Which part of the request do we use to send the auth? 6 + * 7 + * @default 'header' 8 + */ 9 + in?: 'header' | 'query' | 'cookie'; 10 + /** 11 + * Header or query parameter name. 12 + * 13 + * @default 'Authorization' 14 + */ 15 + name?: string; 16 + scheme?: 'basic' | 'bearer'; 17 + type: 'apiKey' | 'http'; 18 + } 19 + 20 + export const getAuthToken = async ( 21 + auth: Auth, 22 + callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken, 23 + ): Promise<string | undefined> => { 24 + const token = 25 + typeof callback === 'function' ? await callback(auth) : callback; 26 + 27 + if (!token) { 28 + return; 29 + } 30 + 31 + if (auth.scheme === 'bearer') { 32 + return `Bearer ${token}`; 33 + } 34 + 35 + if (auth.scheme === 'basic') { 36 + return `Basic ${btoa(token)}`; 37 + } 38 + 39 + return token; 40 + };
+84
examples/openapi-ts-tanstack-svelte-query/src/client/core/bodySerializer.ts
··· 1 + import type { 2 + ArrayStyle, 3 + ObjectStyle, 4 + SerializerOptions, 5 + } from './pathSerializer'; 6 + 7 + export type QuerySerializer = (query: Record<string, unknown>) => string; 8 + 9 + export type BodySerializer = (body: any) => any; 10 + 11 + export interface QuerySerializerOptions { 12 + allowReserved?: boolean; 13 + array?: SerializerOptions<ArrayStyle>; 14 + object?: SerializerOptions<ObjectStyle>; 15 + } 16 + 17 + const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { 18 + if (typeof value === 'string' || value instanceof Blob) { 19 + data.append(key, value); 20 + } else { 21 + data.append(key, JSON.stringify(value)); 22 + } 23 + }; 24 + 25 + const serializeUrlSearchParamsPair = ( 26 + data: URLSearchParams, 27 + key: string, 28 + value: unknown, 29 + ) => { 30 + if (typeof value === 'string') { 31 + data.append(key, value); 32 + } else { 33 + data.append(key, JSON.stringify(value)); 34 + } 35 + }; 36 + 37 + export const formDataBodySerializer = { 38 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 39 + body: T, 40 + ) => { 41 + const data = new FormData(); 42 + 43 + Object.entries(body).forEach(([key, value]) => { 44 + if (value === undefined || value === null) { 45 + return; 46 + } 47 + if (Array.isArray(value)) { 48 + value.forEach((v) => serializeFormDataPair(data, key, v)); 49 + } else { 50 + serializeFormDataPair(data, key, value); 51 + } 52 + }); 53 + 54 + return data; 55 + }, 56 + }; 57 + 58 + export const jsonBodySerializer = { 59 + bodySerializer: <T>(body: T) => 60 + JSON.stringify(body, (key, value) => 61 + typeof value === 'bigint' ? value.toString() : value, 62 + ), 63 + }; 64 + 65 + export const urlSearchParamsBodySerializer = { 66 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 67 + body: T, 68 + ) => { 69 + const data = new URLSearchParams(); 70 + 71 + Object.entries(body).forEach(([key, value]) => { 72 + if (value === undefined || value === null) { 73 + return; 74 + } 75 + if (Array.isArray(value)) { 76 + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); 77 + } else { 78 + serializeUrlSearchParamsPair(data, key, value); 79 + } 80 + }); 81 + 82 + return data.toString(); 83 + }, 84 + };
+141
examples/openapi-ts-tanstack-svelte-query/src/client/core/params.ts
··· 1 + type Slot = 'body' | 'headers' | 'path' | 'query'; 2 + 3 + export type Field = 4 + | { 5 + in: Exclude<Slot, 'body'>; 6 + key: string; 7 + map?: string; 8 + } 9 + | { 10 + in: Extract<Slot, 'body'>; 11 + key?: string; 12 + map?: string; 13 + }; 14 + 15 + export interface Fields { 16 + allowExtra?: Partial<Record<Slot, boolean>>; 17 + args?: ReadonlyArray<Field>; 18 + } 19 + 20 + export type FieldsConfig = ReadonlyArray<Field | Fields>; 21 + 22 + const extraPrefixesMap: Record<string, Slot> = { 23 + $body_: 'body', 24 + $headers_: 'headers', 25 + $path_: 'path', 26 + $query_: 'query', 27 + }; 28 + const extraPrefixes = Object.entries(extraPrefixesMap); 29 + 30 + type KeyMap = Map< 31 + string, 32 + { 33 + in: Slot; 34 + map?: string; 35 + } 36 + >; 37 + 38 + const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { 39 + if (!map) { 40 + map = new Map(); 41 + } 42 + 43 + for (const config of fields) { 44 + if ('in' in config) { 45 + if (config.key) { 46 + map.set(config.key, { 47 + in: config.in, 48 + map: config.map, 49 + }); 50 + } 51 + } else if (config.args) { 52 + buildKeyMap(config.args, map); 53 + } 54 + } 55 + 56 + return map; 57 + }; 58 + 59 + interface Params { 60 + body: unknown; 61 + headers: Record<string, unknown>; 62 + path: Record<string, unknown>; 63 + query: Record<string, unknown>; 64 + } 65 + 66 + const stripEmptySlots = (params: Params) => { 67 + for (const [slot, value] of Object.entries(params)) { 68 + if (value && typeof value === 'object' && !Object.keys(value).length) { 69 + delete params[slot as Slot]; 70 + } 71 + } 72 + }; 73 + 74 + export const buildClientParams = ( 75 + args: ReadonlyArray<unknown>, 76 + fields: FieldsConfig, 77 + ) => { 78 + const params: Params = { 79 + body: {}, 80 + headers: {}, 81 + path: {}, 82 + query: {}, 83 + }; 84 + 85 + const map = buildKeyMap(fields); 86 + 87 + let config: FieldsConfig[number] | undefined; 88 + 89 + for (const [index, arg] of args.entries()) { 90 + if (fields[index]) { 91 + config = fields[index]; 92 + } 93 + 94 + if (!config) { 95 + continue; 96 + } 97 + 98 + if ('in' in config) { 99 + if (config.key) { 100 + const field = map.get(config.key)!; 101 + const name = field.map || config.key; 102 + (params[field.in] as Record<string, unknown>)[name] = arg; 103 + } else { 104 + params.body = arg; 105 + } 106 + } else { 107 + for (const [key, value] of Object.entries(arg ?? {})) { 108 + const field = map.get(key); 109 + 110 + if (field) { 111 + const name = field.map || key; 112 + (params[field.in] as Record<string, unknown>)[name] = value; 113 + } else { 114 + const extra = extraPrefixes.find(([prefix]) => 115 + key.startsWith(prefix), 116 + ); 117 + 118 + if (extra) { 119 + const [prefix, slot] = extra; 120 + (params[slot] as Record<string, unknown>)[ 121 + key.slice(prefix.length) 122 + ] = value; 123 + } else { 124 + for (const [slot, allowed] of Object.entries( 125 + config.allowExtra ?? {}, 126 + )) { 127 + if (allowed) { 128 + (params[slot as Slot] as Record<string, unknown>)[key] = value; 129 + break; 130 + } 131 + } 132 + } 133 + } 134 + } 135 + } 136 + } 137 + 138 + stripEmptySlots(params); 139 + 140 + return params; 141 + };
+179
examples/openapi-ts-tanstack-svelte-query/src/client/core/pathSerializer.ts
··· 1 + interface SerializeOptions<T> 2 + extends SerializePrimitiveOptions, 3 + SerializerOptions<T> {} 4 + 5 + interface SerializePrimitiveOptions { 6 + allowReserved?: boolean; 7 + name: string; 8 + } 9 + 10 + export interface SerializerOptions<T> { 11 + /** 12 + * @default true 13 + */ 14 + explode: boolean; 15 + style: T; 16 + } 17 + 18 + export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; 19 + export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 20 + type MatrixStyle = 'label' | 'matrix' | 'simple'; 21 + export type ObjectStyle = 'form' | 'deepObject'; 22 + type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; 23 + 24 + interface SerializePrimitiveParam extends SerializePrimitiveOptions { 25 + value: string; 26 + } 27 + 28 + export const separatorArrayExplode = (style: ArraySeparatorStyle) => { 29 + switch (style) { 30 + case 'label': 31 + return '.'; 32 + case 'matrix': 33 + return ';'; 34 + case 'simple': 35 + return ','; 36 + default: 37 + return '&'; 38 + } 39 + }; 40 + 41 + export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { 42 + switch (style) { 43 + case 'form': 44 + return ','; 45 + case 'pipeDelimited': 46 + return '|'; 47 + case 'spaceDelimited': 48 + return '%20'; 49 + default: 50 + return ','; 51 + } 52 + }; 53 + 54 + export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { 55 + switch (style) { 56 + case 'label': 57 + return '.'; 58 + case 'matrix': 59 + return ';'; 60 + case 'simple': 61 + return ','; 62 + default: 63 + return '&'; 64 + } 65 + }; 66 + 67 + export const serializeArrayParam = ({ 68 + allowReserved, 69 + explode, 70 + name, 71 + style, 72 + value, 73 + }: SerializeOptions<ArraySeparatorStyle> & { 74 + value: unknown[]; 75 + }) => { 76 + if (!explode) { 77 + const joinedValues = ( 78 + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) 79 + ).join(separatorArrayNoExplode(style)); 80 + switch (style) { 81 + case 'label': 82 + return `.${joinedValues}`; 83 + case 'matrix': 84 + return `;${name}=${joinedValues}`; 85 + case 'simple': 86 + return joinedValues; 87 + default: 88 + return `${name}=${joinedValues}`; 89 + } 90 + } 91 + 92 + const separator = separatorArrayExplode(style); 93 + const joinedValues = value 94 + .map((v) => { 95 + if (style === 'label' || style === 'simple') { 96 + return allowReserved ? v : encodeURIComponent(v as string); 97 + } 98 + 99 + return serializePrimitiveParam({ 100 + allowReserved, 101 + name, 102 + value: v as string, 103 + }); 104 + }) 105 + .join(separator); 106 + return style === 'label' || style === 'matrix' 107 + ? separator + joinedValues 108 + : joinedValues; 109 + }; 110 + 111 + export const serializePrimitiveParam = ({ 112 + allowReserved, 113 + name, 114 + value, 115 + }: SerializePrimitiveParam) => { 116 + if (value === undefined || value === null) { 117 + return ''; 118 + } 119 + 120 + if (typeof value === 'object') { 121 + throw new Error( 122 + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', 123 + ); 124 + } 125 + 126 + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; 127 + }; 128 + 129 + export const serializeObjectParam = ({ 130 + allowReserved, 131 + explode, 132 + name, 133 + style, 134 + value, 135 + valueOnly, 136 + }: SerializeOptions<ObjectSeparatorStyle> & { 137 + value: Record<string, unknown> | Date; 138 + valueOnly?: boolean; 139 + }) => { 140 + if (value instanceof Date) { 141 + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; 142 + } 143 + 144 + if (style !== 'deepObject' && !explode) { 145 + let values: string[] = []; 146 + Object.entries(value).forEach(([key, v]) => { 147 + values = [ 148 + ...values, 149 + key, 150 + allowReserved ? (v as string) : encodeURIComponent(v as string), 151 + ]; 152 + }); 153 + const joinedValues = values.join(','); 154 + switch (style) { 155 + case 'form': 156 + return `${name}=${joinedValues}`; 157 + case 'label': 158 + return `.${joinedValues}`; 159 + case 'matrix': 160 + return `;${name}=${joinedValues}`; 161 + default: 162 + return joinedValues; 163 + } 164 + } 165 + 166 + const separator = separatorObjectExplode(style); 167 + const joinedValues = Object.entries(value) 168 + .map(([key, v]) => 169 + serializePrimitiveParam({ 170 + allowReserved, 171 + name: style === 'deepObject' ? `${name}[${key}]` : key, 172 + value: v as string, 173 + }), 174 + ) 175 + .join(separator); 176 + return style === 'label' || style === 'matrix' 177 + ? separator + joinedValues 178 + : joinedValues; 179 + };
+98
examples/openapi-ts-tanstack-svelte-query/src/client/core/types.ts
··· 1 + import type { Auth, AuthToken } from './auth'; 2 + import type { 3 + BodySerializer, 4 + QuerySerializer, 5 + QuerySerializerOptions, 6 + } from './bodySerializer'; 7 + 8 + export interface Client< 9 + RequestFn = never, 10 + Config = unknown, 11 + MethodFn = never, 12 + BuildUrlFn = never, 13 + > { 14 + /** 15 + * Returns the final request URL. 16 + */ 17 + buildUrl: BuildUrlFn; 18 + connect: MethodFn; 19 + delete: MethodFn; 20 + get: MethodFn; 21 + getConfig: () => Config; 22 + head: MethodFn; 23 + options: MethodFn; 24 + patch: MethodFn; 25 + post: MethodFn; 26 + put: MethodFn; 27 + request: RequestFn; 28 + setConfig: (config: Config) => Config; 29 + trace: MethodFn; 30 + } 31 + 32 + export interface Config { 33 + /** 34 + * Auth token or a function returning auth token. The resolved value will be 35 + * added to the request payload as defined by its `security` array. 36 + */ 37 + auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken; 38 + /** 39 + * A function for serializing request body parameter. By default, 40 + * {@link JSON.stringify()} will be used. 41 + */ 42 + bodySerializer?: BodySerializer | null; 43 + /** 44 + * An object containing any HTTP headers that you want to pre-populate your 45 + * `Headers` object with. 46 + * 47 + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} 48 + */ 49 + headers?: 50 + | RequestInit['headers'] 51 + | Record< 52 + string, 53 + | string 54 + | number 55 + | boolean 56 + | (string | number | boolean)[] 57 + | null 58 + | undefined 59 + | unknown 60 + >; 61 + /** 62 + * The request method. 63 + * 64 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} 65 + */ 66 + method?: 67 + | 'CONNECT' 68 + | 'DELETE' 69 + | 'GET' 70 + | 'HEAD' 71 + | 'OPTIONS' 72 + | 'PATCH' 73 + | 'POST' 74 + | 'PUT' 75 + | 'TRACE'; 76 + /** 77 + * A function for serializing request query parameters. By default, arrays 78 + * will be exploded in form style, objects will be exploded in deepObject 79 + * style, and reserved characters are percent-encoded. 80 + * 81 + * This method will have no effect if the native `paramsSerializer()` Axios 82 + * API function is used. 83 + * 84 + * {@link https://swagger.io/docs/specification/serialization/#query View examples} 85 + */ 86 + querySerializer?: QuerySerializer | QuerySerializerOptions; 87 + /** 88 + * A function transforming response data before it's returned. This is useful 89 + * for post-processing data, e.g. converting ISO strings into Date objects. 90 + */ 91 + responseTransformer?: (data: unknown) => Promise<unknown>; 92 + /** 93 + * A function validating response data. This is useful if you want to ensure 94 + * the response conforms to the desired shape, so it can be safely passed to 95 + * the transformers and returned to the user. 96 + */ 97 + responseValidator?: (data: unknown) => Promise<unknown>; 98 + }
+5 -10
examples/openapi-ts-tanstack-svelte-query/src/client/sdk.gen.ts
··· 1 1 // This file is auto-generated by @hey-api/openapi-ts 2 2 3 - import type { 4 - Client, 5 - Options as ClientOptions, 6 - TDataShape, 7 - } from '@hey-api/client-fetch'; 8 - 3 + import type { Client, Options as ClientOptions, TDataShape } from './client'; 9 4 import { client as _heyApiClient } from './client.gen'; 10 5 import type { 11 6 AddPetData, ··· 106 101 ...options, 107 102 headers: { 108 103 'Content-Type': 'application/json', 109 - ...options?.headers, 104 + ...options.headers, 110 105 }, 111 106 }); 112 107 ··· 132 127 ...options, 133 128 headers: { 134 129 'Content-Type': 'application/json', 135 - ...options?.headers, 130 + ...options.headers, 136 131 }, 137 132 }); 138 133 ··· 273 268 ...options, 274 269 headers: { 275 270 'Content-Type': 'application/octet-stream', 276 - ...options?.headers, 271 + ...options.headers, 277 272 }, 278 273 }); 279 274 ··· 471 466 ...options, 472 467 headers: { 473 468 'Content-Type': 'application/json', 474 - ...options?.headers, 469 + ...options.headers, 475 470 }, 476 471 });
-1
examples/openapi-ts-tanstack-vue-query/package.json
··· 15 15 "typecheck": "vue-tsc --build --force" 16 16 }, 17 17 "dependencies": { 18 - "@hey-api/client-fetch": "workspace:*", 19 18 "@tanstack/vue-query": "5.73.3", 20 19 "@tanstack/vue-query-devtools": "5.73.3", 21 20 "pinia": "2.3.0",
+1 -2
examples/openapi-ts-tanstack-vue-query/src/client/client.gen.ts
··· 5 5 type Config, 6 6 createClient, 7 7 createConfig 8 - } from '@hey-api/client-fetch' 9 - 8 + } from './client' 10 9 import type { ClientOptions } from './types.gen' 11 10 12 11 /**
+173
examples/openapi-ts-tanstack-vue-query/src/client/client/client.ts
··· 1 + import type { Client, Config, RequestOptions } from './types' 2 + import { 3 + buildUrl, 4 + createConfig, 5 + createInterceptors, 6 + getParseAs, 7 + mergeConfigs, 8 + mergeHeaders, 9 + setAuthParams 10 + } from './utils' 11 + 12 + type ReqInit = Omit<RequestInit, 'body' | 'headers'> & { 13 + body?: any 14 + headers: ReturnType<typeof mergeHeaders> 15 + } 16 + 17 + export const createClient = (config: Config = {}): Client => { 18 + let _config = mergeConfigs(createConfig(), config) 19 + 20 + const getConfig = (): Config => ({ ..._config }) 21 + 22 + const setConfig = (config: Config): Config => { 23 + _config = mergeConfigs(_config, config) 24 + return getConfig() 25 + } 26 + 27 + const interceptors = createInterceptors<Request, Response, unknown, RequestOptions>() 28 + 29 + const request: Client['request'] = async (options) => { 30 + const opts = { 31 + ..._config, 32 + ...options, 33 + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, 34 + headers: mergeHeaders(_config.headers, options.headers) 35 + } 36 + 37 + if (opts.security) { 38 + await setAuthParams({ 39 + ...opts, 40 + security: opts.security 41 + }) 42 + } 43 + 44 + if (opts.body && opts.bodySerializer) { 45 + opts.body = opts.bodySerializer(opts.body) 46 + } 47 + 48 + // remove Content-Type header if body is empty to avoid sending invalid requests 49 + if (opts.body === undefined || opts.body === '') { 50 + opts.headers.delete('Content-Type') 51 + } 52 + 53 + const url = buildUrl(opts) 54 + const requestInit: ReqInit = { 55 + redirect: 'follow', 56 + ...opts 57 + } 58 + 59 + let request = new Request(url, requestInit) 60 + 61 + for (const fn of interceptors.request._fns) { 62 + if (fn) { 63 + request = await fn(request, opts) 64 + } 65 + } 66 + 67 + // fetch must be assigned here, otherwise it would throw the error: 68 + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation 69 + const _fetch = opts.fetch! 70 + let response = await _fetch(request) 71 + 72 + for (const fn of interceptors.response._fns) { 73 + if (fn) { 74 + response = await fn(response, request, opts) 75 + } 76 + } 77 + 78 + const result = { 79 + request, 80 + response 81 + } 82 + 83 + if (response.ok) { 84 + if (response.status === 204 || response.headers.get('Content-Length') === '0') { 85 + return opts.responseStyle === 'data' 86 + ? {} 87 + : { 88 + data: {}, 89 + ...result 90 + } 91 + } 92 + 93 + const parseAs = 94 + (opts.parseAs === 'auto' 95 + ? getParseAs(response.headers.get('Content-Type')) 96 + : opts.parseAs) ?? 'json' 97 + 98 + if (parseAs === 'stream') { 99 + return opts.responseStyle === 'data' 100 + ? response.body 101 + : { 102 + data: response.body, 103 + ...result 104 + } 105 + } 106 + 107 + let data = await response[parseAs]() 108 + if (parseAs === 'json') { 109 + if (opts.responseValidator) { 110 + await opts.responseValidator(data) 111 + } 112 + 113 + if (opts.responseTransformer) { 114 + data = await opts.responseTransformer(data) 115 + } 116 + } 117 + 118 + return opts.responseStyle === 'data' 119 + ? data 120 + : { 121 + data, 122 + ...result 123 + } 124 + } 125 + 126 + let error = await response.text() 127 + 128 + try { 129 + error = JSON.parse(error) 130 + } catch { 131 + // noop 132 + } 133 + 134 + let finalError = error 135 + 136 + for (const fn of interceptors.error._fns) { 137 + if (fn) { 138 + finalError = (await fn(error, response, request, opts)) as string 139 + } 140 + } 141 + 142 + finalError = finalError || ({} as string) 143 + 144 + if (opts.throwOnError) { 145 + throw finalError 146 + } 147 + 148 + // TODO: we probably want to return error and improve types 149 + return opts.responseStyle === 'data' 150 + ? undefined 151 + : { 152 + error: finalError, 153 + ...result 154 + } 155 + } 156 + 157 + return { 158 + buildUrl, 159 + connect: (options) => request({ ...options, method: 'CONNECT' }), 160 + delete: (options) => request({ ...options, method: 'DELETE' }), 161 + get: (options) => request({ ...options, method: 'GET' }), 162 + getConfig, 163 + head: (options) => request({ ...options, method: 'HEAD' }), 164 + interceptors, 165 + options: (options) => request({ ...options, method: 'OPTIONS' }), 166 + patch: (options) => request({ ...options, method: 'PATCH' }), 167 + post: (options) => request({ ...options, method: 'POST' }), 168 + put: (options) => request({ ...options, method: 'PUT' }), 169 + request, 170 + setConfig, 171 + trace: (options) => request({ ...options, method: 'TRACE' }) 172 + } 173 + }
+22
examples/openapi-ts-tanstack-vue-query/src/client/client/index.ts
··· 1 + export type { Auth } from '../core/auth' 2 + export type { QuerySerializerOptions } from '../core/bodySerializer' 3 + export { 4 + formDataBodySerializer, 5 + jsonBodySerializer, 6 + urlSearchParamsBodySerializer 7 + } from '../core/bodySerializer' 8 + export { buildClientParams } from '../core/params' 9 + export { createClient } from './client' 10 + export type { 11 + Client, 12 + ClientOptions, 13 + Config, 14 + CreateClientConfig, 15 + Options, 16 + OptionsLegacyParser, 17 + RequestOptions, 18 + RequestResult, 19 + ResponseStyle, 20 + TDataShape 21 + } from './types' 22 + export { createConfig, mergeHeaders } from './utils'
+192
examples/openapi-ts-tanstack-vue-query/src/client/client/types.ts
··· 1 + import type { Auth } from '../core/auth' 2 + import type { Client as CoreClient, Config as CoreConfig } from '../core/types' 3 + import type { Middleware } from './utils' 4 + 5 + export type ResponseStyle = 'data' | 'fields' 6 + 7 + export interface Config<T extends ClientOptions = ClientOptions> 8 + extends Omit<RequestInit, 'body' | 'headers' | 'method'>, 9 + CoreConfig { 10 + /** 11 + * Base URL for all requests made by this client. 12 + */ 13 + baseUrl?: T['baseUrl'] 14 + /** 15 + * Fetch API implementation. You can use this option to provide a custom 16 + * fetch instance. 17 + * 18 + * @default globalThis.fetch 19 + */ 20 + fetch?: (request: Request) => ReturnType<typeof fetch> 21 + /** 22 + * Please don't use the Fetch client for Next.js applications. The `next` 23 + * options won't have any effect. 24 + * 25 + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. 26 + */ 27 + next?: never 28 + /** 29 + * Return the response data parsed in a specified format. By default, `auto` 30 + * will infer the appropriate method from the `Content-Type` response header. 31 + * You can override this behavior with any of the {@link Body} methods. 32 + * Select `stream` if you don't want to parse response data at all. 33 + * 34 + * @default 'auto' 35 + */ 36 + parseAs?: Exclude<keyof Body, 'body' | 'bodyUsed'> | 'auto' | 'stream' 37 + /** 38 + * Should we return only data or multiple fields (data, error, response, etc.)? 39 + * 40 + * @default 'fields' 41 + */ 42 + responseStyle?: ResponseStyle 43 + /** 44 + * Throw an error instead of returning it in the response? 45 + * 46 + * @default false 47 + */ 48 + throwOnError?: T['throwOnError'] 49 + } 50 + 51 + export interface RequestOptions< 52 + TResponseStyle extends ResponseStyle = 'fields', 53 + ThrowOnError extends boolean = boolean, 54 + Url extends string = string 55 + > extends Config<{ 56 + responseStyle: TResponseStyle 57 + throwOnError: ThrowOnError 58 + }> { 59 + /** 60 + * Any body that you want to add to your request. 61 + * 62 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} 63 + */ 64 + body?: unknown 65 + path?: Record<string, unknown> 66 + query?: Record<string, unknown> 67 + /** 68 + * Security mechanism(s) to use for the request. 69 + */ 70 + security?: ReadonlyArray<Auth> 71 + url: Url 72 + } 73 + 74 + export type RequestResult< 75 + TData = unknown, 76 + TError = unknown, 77 + ThrowOnError extends boolean = boolean, 78 + TResponseStyle extends ResponseStyle = 'fields' 79 + > = ThrowOnError extends true 80 + ? Promise< 81 + TResponseStyle extends 'data' 82 + ? TData extends Record<string, unknown> 83 + ? TData[keyof TData] 84 + : TData 85 + : { 86 + data: TData extends Record<string, unknown> ? TData[keyof TData] : TData 87 + request: Request 88 + response: Response 89 + } 90 + > 91 + : Promise< 92 + TResponseStyle extends 'data' 93 + ? (TData extends Record<string, unknown> ? TData[keyof TData] : TData) | undefined 94 + : ( 95 + | { 96 + data: TData extends Record<string, unknown> ? TData[keyof TData] : TData 97 + error: undefined 98 + } 99 + | { 100 + data: undefined 101 + error: TError extends Record<string, unknown> ? TError[keyof TError] : TError 102 + } 103 + ) & { 104 + request: Request 105 + response: Response 106 + } 107 + > 108 + 109 + export interface ClientOptions { 110 + baseUrl?: string 111 + responseStyle?: ResponseStyle 112 + throwOnError?: boolean 113 + } 114 + 115 + type MethodFn = < 116 + TData = unknown, 117 + TError = unknown, 118 + ThrowOnError extends boolean = false, 119 + TResponseStyle extends ResponseStyle = 'fields' 120 + >( 121 + options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'> 122 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle> 123 + 124 + type RequestFn = < 125 + TData = unknown, 126 + TError = unknown, 127 + ThrowOnError extends boolean = false, 128 + TResponseStyle extends ResponseStyle = 'fields' 129 + >( 130 + options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'> & 131 + Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, 'method'> 132 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle> 133 + 134 + type BuildUrlFn = < 135 + TData extends { 136 + body?: unknown 137 + path?: Record<string, unknown> 138 + query?: Record<string, unknown> 139 + url: string 140 + } 141 + >( 142 + options: Pick<TData, 'url'> & Options<TData> 143 + ) => string 144 + 145 + export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & { 146 + interceptors: Middleware<Request, Response, unknown, RequestOptions> 147 + } 148 + 149 + /** 150 + * The `createClientConfig()` function will be called on client initialization 151 + * and the returned object will become the client's initial configuration. 152 + * 153 + * You may want to initialize your client this way instead of calling 154 + * `setConfig()`. This is useful for example if you're using Next.js 155 + * to ensure your client always has the correct values. 156 + */ 157 + export type CreateClientConfig<T extends ClientOptions = ClientOptions> = ( 158 + override?: Config<ClientOptions & T> 159 + ) => Config<Required<ClientOptions> & T> 160 + 161 + export interface TDataShape { 162 + body?: unknown 163 + headers?: unknown 164 + path?: unknown 165 + query?: unknown 166 + url: string 167 + } 168 + 169 + type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>> 170 + 171 + export type Options< 172 + TData extends TDataShape = TDataShape, 173 + ThrowOnError extends boolean = boolean, 174 + TResponseStyle extends ResponseStyle = 'fields' 175 + > = OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'body' | 'path' | 'query' | 'url'> & 176 + Omit<TData, 'url'> 177 + 178 + export type OptionsLegacyParser< 179 + TData = unknown, 180 + ThrowOnError extends boolean = boolean, 181 + TResponseStyle extends ResponseStyle = 'fields' 182 + > = TData extends { body?: any } 183 + ? TData extends { headers?: any } 184 + ? OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'body' | 'headers' | 'url'> & TData 185 + : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'body' | 'url'> & 186 + TData & 187 + Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'headers'> 188 + : TData extends { headers?: any } 189 + ? OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'headers' | 'url'> & 190 + TData & 191 + Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'body'> 192 + : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'url'> & TData
+392
examples/openapi-ts-tanstack-vue-query/src/client/client/utils.ts
··· 1 + import { getAuthToken } from '../core/auth' 2 + import type { QuerySerializer, QuerySerializerOptions } from '../core/bodySerializer' 3 + import { jsonBodySerializer } from '../core/bodySerializer' 4 + import { 5 + serializeArrayParam, 6 + serializeObjectParam, 7 + serializePrimitiveParam 8 + } from '../core/pathSerializer' 9 + import type { Client, ClientOptions, Config, RequestOptions } from './types' 10 + 11 + interface PathSerializer { 12 + path: Record<string, unknown> 13 + url: string 14 + } 15 + 16 + const PATH_PARAM_RE = /\{[^{}]+\}/g 17 + 18 + type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited' 19 + type MatrixStyle = 'label' | 'matrix' | 'simple' 20 + type ArraySeparatorStyle = ArrayStyle | MatrixStyle 21 + 22 + const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { 23 + let url = _url 24 + const matches = _url.match(PATH_PARAM_RE) 25 + if (matches) { 26 + for (const match of matches) { 27 + let explode = false 28 + let name = match.substring(1, match.length - 1) 29 + let style: ArraySeparatorStyle = 'simple' 30 + 31 + if (name.endsWith('*')) { 32 + explode = true 33 + name = name.substring(0, name.length - 1) 34 + } 35 + 36 + if (name.startsWith('.')) { 37 + name = name.substring(1) 38 + style = 'label' 39 + } else if (name.startsWith(';')) { 40 + name = name.substring(1) 41 + style = 'matrix' 42 + } 43 + 44 + const value = path[name] 45 + 46 + if (value === undefined || value === null) { 47 + continue 48 + } 49 + 50 + if (Array.isArray(value)) { 51 + url = url.replace(match, serializeArrayParam({ explode, name, style, value })) 52 + continue 53 + } 54 + 55 + if (typeof value === 'object') { 56 + url = url.replace( 57 + match, 58 + serializeObjectParam({ 59 + explode, 60 + name, 61 + style, 62 + value: value as Record<string, unknown>, 63 + valueOnly: true 64 + }) 65 + ) 66 + continue 67 + } 68 + 69 + if (style === 'matrix') { 70 + url = url.replace( 71 + match, 72 + `;${serializePrimitiveParam({ 73 + name, 74 + value: value as string 75 + })}` 76 + ) 77 + continue 78 + } 79 + 80 + const replaceValue = encodeURIComponent( 81 + style === 'label' ? `.${value as string}` : (value as string) 82 + ) 83 + url = url.replace(match, replaceValue) 84 + } 85 + } 86 + return url 87 + } 88 + 89 + export const createQuerySerializer = <T = unknown>({ 90 + allowReserved, 91 + array, 92 + object 93 + }: QuerySerializerOptions = {}) => { 94 + const querySerializer = (queryParams: T) => { 95 + const search: string[] = [] 96 + if (queryParams && typeof queryParams === 'object') { 97 + for (const name in queryParams) { 98 + const value = queryParams[name] 99 + 100 + if (value === undefined || value === null) { 101 + continue 102 + } 103 + 104 + if (Array.isArray(value)) { 105 + const serializedArray = serializeArrayParam({ 106 + allowReserved, 107 + explode: true, 108 + name, 109 + style: 'form', 110 + value, 111 + ...array 112 + }) 113 + if (serializedArray) search.push(serializedArray) 114 + } else if (typeof value === 'object') { 115 + const serializedObject = serializeObjectParam({ 116 + allowReserved, 117 + explode: true, 118 + name, 119 + style: 'deepObject', 120 + value: value as Record<string, unknown>, 121 + ...object 122 + }) 123 + if (serializedObject) search.push(serializedObject) 124 + } else { 125 + const serializedPrimitive = serializePrimitiveParam({ 126 + allowReserved, 127 + name, 128 + value: value as string 129 + }) 130 + if (serializedPrimitive) search.push(serializedPrimitive) 131 + } 132 + } 133 + } 134 + return search.join('&') 135 + } 136 + return querySerializer 137 + } 138 + 139 + /** 140 + * Infers parseAs value from provided Content-Type header. 141 + */ 142 + export const getParseAs = (contentType: string | null): Exclude<Config['parseAs'], 'auto'> => { 143 + if (!contentType) { 144 + // If no Content-Type header is provided, the best we can do is return the raw response body, 145 + // which is effectively the same as the 'stream' option. 146 + return 'stream' 147 + } 148 + 149 + const cleanContent = contentType.split(';')[0]?.trim() 150 + 151 + if (!cleanContent) { 152 + return 153 + } 154 + 155 + if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { 156 + return 'json' 157 + } 158 + 159 + if (cleanContent === 'multipart/form-data') { 160 + return 'formData' 161 + } 162 + 163 + if ( 164 + ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) 165 + ) { 166 + return 'blob' 167 + } 168 + 169 + if (cleanContent.startsWith('text/')) { 170 + return 'text' 171 + } 172 + } 173 + 174 + export const setAuthParams = async ({ 175 + security, 176 + ...options 177 + }: Pick<Required<RequestOptions>, 'security'> & 178 + Pick<RequestOptions, 'auth' | 'query'> & { 179 + headers: Headers 180 + }) => { 181 + for (const auth of security) { 182 + const token = await getAuthToken(auth, options.auth) 183 + 184 + if (!token) { 185 + continue 186 + } 187 + 188 + const name = auth.name ?? 'Authorization' 189 + 190 + switch (auth.in) { 191 + case 'query': 192 + if (!options.query) { 193 + options.query = {} 194 + } 195 + options.query[name] = token 196 + break 197 + case 'cookie': 198 + options.headers.append('Cookie', `${name}=${token}`) 199 + break 200 + case 'header': 201 + default: 202 + options.headers.set(name, token) 203 + break 204 + } 205 + 206 + return 207 + } 208 + } 209 + 210 + export const buildUrl: Client['buildUrl'] = (options) => { 211 + const url = getUrl({ 212 + baseUrl: options.baseUrl as string, 213 + path: options.path, 214 + query: options.query, 215 + querySerializer: 216 + typeof options.querySerializer === 'function' 217 + ? options.querySerializer 218 + : createQuerySerializer(options.querySerializer), 219 + url: options.url 220 + }) 221 + return url 222 + } 223 + 224 + export const getUrl = ({ 225 + baseUrl, 226 + path, 227 + query, 228 + querySerializer, 229 + url: _url 230 + }: { 231 + baseUrl?: string 232 + path?: Record<string, unknown> 233 + query?: Record<string, unknown> 234 + querySerializer: QuerySerializer 235 + url: string 236 + }) => { 237 + const pathUrl = _url.startsWith('/') ? _url : `/${_url}` 238 + let url = (baseUrl ?? '') + pathUrl 239 + if (path) { 240 + url = defaultPathSerializer({ path, url }) 241 + } 242 + let search = query ? querySerializer(query) : '' 243 + if (search.startsWith('?')) { 244 + search = search.substring(1) 245 + } 246 + if (search) { 247 + url += `?${search}` 248 + } 249 + return url 250 + } 251 + 252 + export const mergeConfigs = (a: Config, b: Config): Config => { 253 + const config = { ...a, ...b } 254 + if (config.baseUrl?.endsWith('/')) { 255 + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1) 256 + } 257 + config.headers = mergeHeaders(a.headers, b.headers) 258 + return config 259 + } 260 + 261 + export const mergeHeaders = ( 262 + ...headers: Array<Required<Config>['headers'] | undefined> 263 + ): Headers => { 264 + const mergedHeaders = new Headers() 265 + for (const header of headers) { 266 + if (!header || typeof header !== 'object') { 267 + continue 268 + } 269 + 270 + const iterator = header instanceof Headers ? header.entries() : Object.entries(header) 271 + 272 + for (const [key, value] of iterator) { 273 + if (value === null) { 274 + mergedHeaders.delete(key) 275 + } else if (Array.isArray(value)) { 276 + for (const v of value) { 277 + mergedHeaders.append(key, v as string) 278 + } 279 + } else if (value !== undefined) { 280 + // assume object headers are meant to be JSON stringified, i.e. their 281 + // content value in OpenAPI specification is 'application/json' 282 + mergedHeaders.set( 283 + key, 284 + typeof value === 'object' ? JSON.stringify(value) : (value as string) 285 + ) 286 + } 287 + } 288 + } 289 + return mergedHeaders 290 + } 291 + 292 + type ErrInterceptor<Err, Res, Req, Options> = ( 293 + error: Err, 294 + response: Res, 295 + request: Req, 296 + options: Options 297 + ) => Err | Promise<Err> 298 + 299 + type ReqInterceptor<Req, Options> = (request: Req, options: Options) => Req | Promise<Req> 300 + 301 + type ResInterceptor<Res, Req, Options> = ( 302 + response: Res, 303 + request: Req, 304 + options: Options 305 + ) => Res | Promise<Res> 306 + 307 + class Interceptors<Interceptor> { 308 + _fns: (Interceptor | null)[] 309 + 310 + constructor() { 311 + this._fns = [] 312 + } 313 + 314 + clear() { 315 + this._fns = [] 316 + } 317 + 318 + getInterceptorIndex(id: number | Interceptor): number { 319 + if (typeof id === 'number') { 320 + return this._fns[id] ? id : -1 321 + } else { 322 + return this._fns.indexOf(id) 323 + } 324 + } 325 + exists(id: number | Interceptor) { 326 + const index = this.getInterceptorIndex(id) 327 + return !!this._fns[index] 328 + } 329 + 330 + eject(id: number | Interceptor) { 331 + const index = this.getInterceptorIndex(id) 332 + if (this._fns[index]) { 333 + this._fns[index] = null 334 + } 335 + } 336 + 337 + update(id: number | Interceptor, fn: Interceptor) { 338 + const index = this.getInterceptorIndex(id) 339 + if (this._fns[index]) { 340 + this._fns[index] = fn 341 + return id 342 + } else { 343 + return false 344 + } 345 + } 346 + 347 + use(fn: Interceptor) { 348 + this._fns = [...this._fns, fn] 349 + return this._fns.length - 1 350 + } 351 + } 352 + 353 + // `createInterceptors()` response, meant for external use as it does not 354 + // expose internals 355 + export interface Middleware<Req, Res, Err, Options> { 356 + error: Pick<Interceptors<ErrInterceptor<Err, Res, Req, Options>>, 'eject' | 'use'> 357 + request: Pick<Interceptors<ReqInterceptor<Req, Options>>, 'eject' | 'use'> 358 + response: Pick<Interceptors<ResInterceptor<Res, Req, Options>>, 'eject' | 'use'> 359 + } 360 + 361 + // do not add `Middleware` as return type so we can use _fns internally 362 + export const createInterceptors = <Req, Res, Err, Options>() => ({ 363 + error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(), 364 + request: new Interceptors<ReqInterceptor<Req, Options>>(), 365 + response: new Interceptors<ResInterceptor<Res, Req, Options>>() 366 + }) 367 + 368 + const defaultQuerySerializer = createQuerySerializer({ 369 + allowReserved: false, 370 + array: { 371 + explode: true, 372 + style: 'form' 373 + }, 374 + object: { 375 + explode: true, 376 + style: 'deepObject' 377 + } 378 + }) 379 + 380 + const defaultHeaders = { 381 + 'Content-Type': 'application/json' 382 + } 383 + 384 + export const createConfig = <T extends ClientOptions = ClientOptions>( 385 + override: Config<Omit<ClientOptions, keyof T> & T> = {} 386 + ): Config<Omit<ClientOptions, keyof T> & T> => ({ 387 + ...jsonBodySerializer, 388 + headers: defaultHeaders, 389 + parseAs: 'auto', 390 + querySerializer: defaultQuerySerializer, 391 + ...override 392 + })
+39
examples/openapi-ts-tanstack-vue-query/src/client/core/auth.ts
··· 1 + export type AuthToken = string | undefined 2 + 3 + export interface Auth { 4 + /** 5 + * Which part of the request do we use to send the auth? 6 + * 7 + * @default 'header' 8 + */ 9 + in?: 'header' | 'query' | 'cookie' 10 + /** 11 + * Header or query parameter name. 12 + * 13 + * @default 'Authorization' 14 + */ 15 + name?: string 16 + scheme?: 'basic' | 'bearer' 17 + type: 'apiKey' | 'http' 18 + } 19 + 20 + export const getAuthToken = async ( 21 + auth: Auth, 22 + callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken 23 + ): Promise<string | undefined> => { 24 + const token = typeof callback === 'function' ? await callback(auth) : callback 25 + 26 + if (!token) { 27 + return 28 + } 29 + 30 + if (auth.scheme === 'bearer') { 31 + return `Bearer ${token}` 32 + } 33 + 34 + if (auth.scheme === 'basic') { 35 + return `Basic ${btoa(token)}` 36 + } 37 + 38 + return token 39 + }
+70
examples/openapi-ts-tanstack-vue-query/src/client/core/bodySerializer.ts
··· 1 + import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer' 2 + 3 + export type QuerySerializer = (query: Record<string, unknown>) => string 4 + 5 + export type BodySerializer = (body: any) => any 6 + 7 + export interface QuerySerializerOptions { 8 + allowReserved?: boolean 9 + array?: SerializerOptions<ArrayStyle> 10 + object?: SerializerOptions<ObjectStyle> 11 + } 12 + 13 + const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { 14 + if (typeof value === 'string' || value instanceof Blob) { 15 + data.append(key, value) 16 + } else { 17 + data.append(key, JSON.stringify(value)) 18 + } 19 + } 20 + 21 + const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown) => { 22 + if (typeof value === 'string') { 23 + data.append(key, value) 24 + } else { 25 + data.append(key, JSON.stringify(value)) 26 + } 27 + } 28 + 29 + export const formDataBodySerializer = { 30 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(body: T) => { 31 + const data = new FormData() 32 + 33 + Object.entries(body).forEach(([key, value]) => { 34 + if (value === undefined || value === null) { 35 + return 36 + } 37 + if (Array.isArray(value)) { 38 + value.forEach((v) => serializeFormDataPair(data, key, v)) 39 + } else { 40 + serializeFormDataPair(data, key, value) 41 + } 42 + }) 43 + 44 + return data 45 + } 46 + } 47 + 48 + export const jsonBodySerializer = { 49 + bodySerializer: <T>(body: T) => 50 + JSON.stringify(body, (key, value) => (typeof value === 'bigint' ? value.toString() : value)) 51 + } 52 + 53 + export const urlSearchParamsBodySerializer = { 54 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(body: T) => { 55 + const data = new URLSearchParams() 56 + 57 + Object.entries(body).forEach(([key, value]) => { 58 + if (value === undefined || value === null) { 59 + return 60 + } 61 + if (Array.isArray(value)) { 62 + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)) 63 + } else { 64 + serializeUrlSearchParamsPair(data, key, value) 65 + } 66 + }) 67 + 68 + return data.toString() 69 + } 70 + }
+132
examples/openapi-ts-tanstack-vue-query/src/client/core/params.ts
··· 1 + type Slot = 'body' | 'headers' | 'path' | 'query' 2 + 3 + export type Field = 4 + | { 5 + in: Exclude<Slot, 'body'> 6 + key: string 7 + map?: string 8 + } 9 + | { 10 + in: Extract<Slot, 'body'> 11 + key?: string 12 + map?: string 13 + } 14 + 15 + export interface Fields { 16 + allowExtra?: Partial<Record<Slot, boolean>> 17 + args?: ReadonlyArray<Field> 18 + } 19 + 20 + export type FieldsConfig = ReadonlyArray<Field | Fields> 21 + 22 + const extraPrefixesMap: Record<string, Slot> = { 23 + $body_: 'body', 24 + $headers_: 'headers', 25 + $path_: 'path', 26 + $query_: 'query' 27 + } 28 + const extraPrefixes = Object.entries(extraPrefixesMap) 29 + 30 + type KeyMap = Map< 31 + string, 32 + { 33 + in: Slot 34 + map?: string 35 + } 36 + > 37 + 38 + const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { 39 + if (!map) { 40 + map = new Map() 41 + } 42 + 43 + for (const config of fields) { 44 + if ('in' in config) { 45 + if (config.key) { 46 + map.set(config.key, { 47 + in: config.in, 48 + map: config.map 49 + }) 50 + } 51 + } else if (config.args) { 52 + buildKeyMap(config.args, map) 53 + } 54 + } 55 + 56 + return map 57 + } 58 + 59 + interface Params { 60 + body: unknown 61 + headers: Record<string, unknown> 62 + path: Record<string, unknown> 63 + query: Record<string, unknown> 64 + } 65 + 66 + const stripEmptySlots = (params: Params) => { 67 + for (const [slot, value] of Object.entries(params)) { 68 + if (value && typeof value === 'object' && !Object.keys(value).length) { 69 + delete params[slot as Slot] 70 + } 71 + } 72 + } 73 + 74 + export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsConfig) => { 75 + const params: Params = { 76 + body: {}, 77 + headers: {}, 78 + path: {}, 79 + query: {} 80 + } 81 + 82 + const map = buildKeyMap(fields) 83 + 84 + let config: FieldsConfig[number] | undefined 85 + 86 + for (const [index, arg] of args.entries()) { 87 + if (fields[index]) { 88 + config = fields[index] 89 + } 90 + 91 + if (!config) { 92 + continue 93 + } 94 + 95 + if ('in' in config) { 96 + if (config.key) { 97 + const field = map.get(config.key)! 98 + const name = field.map || config.key 99 + ;(params[field.in] as Record<string, unknown>)[name] = arg 100 + } else { 101 + params.body = arg 102 + } 103 + } else { 104 + for (const [key, value] of Object.entries(arg ?? {})) { 105 + const field = map.get(key) 106 + 107 + if (field) { 108 + const name = field.map || key 109 + ;(params[field.in] as Record<string, unknown>)[name] = value 110 + } else { 111 + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)) 112 + 113 + if (extra) { 114 + const [prefix, slot] = extra 115 + ;(params[slot] as Record<string, unknown>)[key.slice(prefix.length)] = value 116 + } else { 117 + for (const [slot, allowed] of Object.entries(config.allowExtra ?? {})) { 118 + if (allowed) { 119 + ;(params[slot as Slot] as Record<string, unknown>)[key] = value 120 + break 121 + } 122 + } 123 + } 124 + } 125 + } 126 + } 127 + } 128 + 129 + stripEmptySlots(params) 130 + 131 + return params 132 + }
+169
examples/openapi-ts-tanstack-vue-query/src/client/core/pathSerializer.ts
··· 1 + interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {} 2 + 3 + interface SerializePrimitiveOptions { 4 + allowReserved?: boolean 5 + name: string 6 + } 7 + 8 + export interface SerializerOptions<T> { 9 + /** 10 + * @default true 11 + */ 12 + explode: boolean 13 + style: T 14 + } 15 + 16 + export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited' 17 + export type ArraySeparatorStyle = ArrayStyle | MatrixStyle 18 + type MatrixStyle = 'label' | 'matrix' | 'simple' 19 + export type ObjectStyle = 'form' | 'deepObject' 20 + type ObjectSeparatorStyle = ObjectStyle | MatrixStyle 21 + 22 + interface SerializePrimitiveParam extends SerializePrimitiveOptions { 23 + value: string 24 + } 25 + 26 + export const separatorArrayExplode = (style: ArraySeparatorStyle) => { 27 + switch (style) { 28 + case 'label': 29 + return '.' 30 + case 'matrix': 31 + return ';' 32 + case 'simple': 33 + return ',' 34 + default: 35 + return '&' 36 + } 37 + } 38 + 39 + export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { 40 + switch (style) { 41 + case 'form': 42 + return ',' 43 + case 'pipeDelimited': 44 + return '|' 45 + case 'spaceDelimited': 46 + return '%20' 47 + default: 48 + return ',' 49 + } 50 + } 51 + 52 + export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { 53 + switch (style) { 54 + case 'label': 55 + return '.' 56 + case 'matrix': 57 + return ';' 58 + case 'simple': 59 + return ',' 60 + default: 61 + return '&' 62 + } 63 + } 64 + 65 + export const serializeArrayParam = ({ 66 + allowReserved, 67 + explode, 68 + name, 69 + style, 70 + value 71 + }: SerializeOptions<ArraySeparatorStyle> & { 72 + value: unknown[] 73 + }) => { 74 + if (!explode) { 75 + const joinedValues = ( 76 + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) 77 + ).join(separatorArrayNoExplode(style)) 78 + switch (style) { 79 + case 'label': 80 + return `.${joinedValues}` 81 + case 'matrix': 82 + return `;${name}=${joinedValues}` 83 + case 'simple': 84 + return joinedValues 85 + default: 86 + return `${name}=${joinedValues}` 87 + } 88 + } 89 + 90 + const separator = separatorArrayExplode(style) 91 + const joinedValues = value 92 + .map((v) => { 93 + if (style === 'label' || style === 'simple') { 94 + return allowReserved ? v : encodeURIComponent(v as string) 95 + } 96 + 97 + return serializePrimitiveParam({ 98 + allowReserved, 99 + name, 100 + value: v as string 101 + }) 102 + }) 103 + .join(separator) 104 + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues 105 + } 106 + 107 + export const serializePrimitiveParam = ({ 108 + allowReserved, 109 + name, 110 + value 111 + }: SerializePrimitiveParam) => { 112 + if (value === undefined || value === null) { 113 + return '' 114 + } 115 + 116 + if (typeof value === 'object') { 117 + throw new Error( 118 + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.' 119 + ) 120 + } 121 + 122 + return `${name}=${allowReserved ? value : encodeURIComponent(value)}` 123 + } 124 + 125 + export const serializeObjectParam = ({ 126 + allowReserved, 127 + explode, 128 + name, 129 + style, 130 + value, 131 + valueOnly 132 + }: SerializeOptions<ObjectSeparatorStyle> & { 133 + value: Record<string, unknown> | Date 134 + valueOnly?: boolean 135 + }) => { 136 + if (value instanceof Date) { 137 + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}` 138 + } 139 + 140 + if (style !== 'deepObject' && !explode) { 141 + let values: string[] = [] 142 + Object.entries(value).forEach(([key, v]) => { 143 + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)] 144 + }) 145 + const joinedValues = values.join(',') 146 + switch (style) { 147 + case 'form': 148 + return `${name}=${joinedValues}` 149 + case 'label': 150 + return `.${joinedValues}` 151 + case 'matrix': 152 + return `;${name}=${joinedValues}` 153 + default: 154 + return joinedValues 155 + } 156 + } 157 + 158 + const separator = separatorObjectExplode(style) 159 + const joinedValues = Object.entries(value) 160 + .map(([key, v]) => 161 + serializePrimitiveParam({ 162 + allowReserved, 163 + name: style === 'deepObject' ? `${name}[${key}]` : key, 164 + value: v as string 165 + }) 166 + ) 167 + .join(separator) 168 + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues 169 + }
+74
examples/openapi-ts-tanstack-vue-query/src/client/core/types.ts
··· 1 + import type { Auth, AuthToken } from './auth' 2 + import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer' 3 + 4 + export interface Client<RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never> { 5 + /** 6 + * Returns the final request URL. 7 + */ 8 + buildUrl: BuildUrlFn 9 + connect: MethodFn 10 + delete: MethodFn 11 + get: MethodFn 12 + getConfig: () => Config 13 + head: MethodFn 14 + options: MethodFn 15 + patch: MethodFn 16 + post: MethodFn 17 + put: MethodFn 18 + request: RequestFn 19 + setConfig: (config: Config) => Config 20 + trace: MethodFn 21 + } 22 + 23 + export interface Config { 24 + /** 25 + * Auth token or a function returning auth token. The resolved value will be 26 + * added to the request payload as defined by its `security` array. 27 + */ 28 + auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken 29 + /** 30 + * A function for serializing request body parameter. By default, 31 + * {@link JSON.stringify()} will be used. 32 + */ 33 + bodySerializer?: BodySerializer | null 34 + /** 35 + * An object containing any HTTP headers that you want to pre-populate your 36 + * `Headers` object with. 37 + * 38 + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} 39 + */ 40 + headers?: 41 + | RequestInit['headers'] 42 + | Record< 43 + string, 44 + string | number | boolean | (string | number | boolean)[] | null | undefined | unknown 45 + > 46 + /** 47 + * The request method. 48 + * 49 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} 50 + */ 51 + method?: 'CONNECT' | 'DELETE' | 'GET' | 'HEAD' | 'OPTIONS' | 'PATCH' | 'POST' | 'PUT' | 'TRACE' 52 + /** 53 + * A function for serializing request query parameters. By default, arrays 54 + * will be exploded in form style, objects will be exploded in deepObject 55 + * style, and reserved characters are percent-encoded. 56 + * 57 + * This method will have no effect if the native `paramsSerializer()` Axios 58 + * API function is used. 59 + * 60 + * {@link https://swagger.io/docs/specification/serialization/#query View examples} 61 + */ 62 + querySerializer?: QuerySerializer | QuerySerializerOptions 63 + /** 64 + * A function transforming response data before it's returned. This is useful 65 + * for post-processing data, e.g. converting ISO strings into Date objects. 66 + */ 67 + responseTransformer?: (data: unknown) => Promise<unknown> 68 + /** 69 + * A function validating response data. This is useful if you want to ensure 70 + * the response conforms to the desired shape, so it can be safely passed to 71 + * the transformers and returned to the user. 72 + */ 73 + responseValidator?: (data: unknown) => Promise<unknown> 74 + }
+5 -6
examples/openapi-ts-tanstack-vue-query/src/client/sdk.gen.ts
··· 1 1 // This file is auto-generated by @hey-api/openapi-ts 2 2 3 - import type { Client, Options as ClientOptions, TDataShape } from '@hey-api/client-fetch' 4 - 3 + import type { Client, Options as ClientOptions, TDataShape } from './client' 5 4 import { client as _heyApiClient } from './client.gen' 6 5 import type { 7 6 AddPetData, ··· 98 97 ...options, 99 98 headers: { 100 99 'Content-Type': 'application/json', 101 - ...options?.headers 100 + ...options.headers 102 101 } 103 102 }) 104 103 ··· 120 119 ...options, 121 120 headers: { 122 121 'Content-Type': 'application/json', 123 - ...options?.headers 122 + ...options.headers 124 123 } 125 124 }) 126 125 ··· 249 248 ...options, 250 249 headers: { 251 250 'Content-Type': 'application/octet-stream', 252 - ...options?.headers 251 + ...options.headers 253 252 } 254 253 }) 255 254 ··· 407 406 ...options, 408 407 headers: { 409 408 'Content-Type': 'application/json', 410 - ...options?.headers 409 + ...options.headers 411 410 } 412 411 })
+1 -1
examples/openapi-ts-tanstack-vue-query/src/views/TanstackExample.vue
··· 1 1 <script lang="ts" setup> 2 2 import type { Pet } from '@/client' 3 + import { createClient } from '@client/client' 3 4 import { PetSchema } from '@/client/schemas.gen' 4 5 import { 5 6 addPetMutation, 6 7 getPetByIdOptions, 7 8 updatePetMutation 8 9 } from '@/client/@tanstack/vue-query.gen' 9 - import { createClient } from '@hey-api/client-fetch' 10 10 import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query' 11 11 import { computed, ref, watch } from 'vue' 12 12
-3
package.json
··· 46 46 "@changesets/changelog-github": "0.5.0", 47 47 "@changesets/cli": "2.27.8", 48 48 "@config/vite-base": "workspace:*", 49 - "@hey-api/client-axios": "workspace:*", 50 49 "@hey-api/client-custom": "workspace:*", 51 - "@hey-api/client-fetch": "workspace:*", 52 - "@hey-api/client-next": "workspace:*", 53 50 "@hey-api/client-nuxt": "workspace:*", 54 51 "@types/node": "22.10.5", 55 52 "@typescript-eslint/eslint-plugin": "8.29.1",
-1
packages/client-custom/package.json
··· 67 67 }, 68 68 "devDependencies": { 69 69 "@config/vite-base": "workspace:*", 70 - "@hey-api/client-core": "workspace:*", 71 70 "@hey-api/openapi-ts": "workspace:*" 72 71 } 73 72 }
-1
packages/client-nuxt/package.json
··· 67 67 }, 68 68 "devDependencies": { 69 69 "@config/vite-base": "workspace:*", 70 - "@hey-api/client-core": "workspace:*", 71 70 "@hey-api/openapi-ts": "workspace:*", 72 71 "@nuxt/test-utils": "3.17.2", 73 72 "vite": "6.2.7",
+5 -12
packages/openapi-ts/README.md
··· 120 120 ```sh 121 121 npx @hey-api/openapi-ts \ 122 122 -i https://get.heyapi.dev/hey-api/backend \ 123 - -o src/client \ 124 - -c @hey-api/client-fetch 123 + -o src/client 125 124 ``` 126 125 127 126 Congratulations on creating your first client! 🎉 You can learn more about the generated files on the [Output](https://heyapi.dev/openapi-ts/output) page. 128 - 129 - Before you can make API requests with the client you've just created, you need to install `@hey-api/client-fetch` and configure it. 130 127 131 128 ## Installation 132 129 133 130 #### npm 134 131 135 132 ```sh 136 - npm install @hey-api/client-fetch && npm install @hey-api/openapi-ts -D 133 + npm install @hey-api/openapi-ts -D 137 134 ``` 138 135 139 136 #### pnpm 140 137 141 138 ```sh 142 - pnpm add @hey-api/client-fetch && pnpm add @hey-api/openapi-ts -D 139 + pnpm add @hey-api/openapi-ts -D 143 140 ``` 144 141 145 142 #### yarn 146 143 147 144 ```sh 148 - yarn add @hey-api/client-fetch && yarn add @hey-api/openapi-ts -D 145 + yarn add @hey-api/openapi-ts -D 149 146 ``` 150 147 151 148 #### bun 152 149 153 150 ```sh 154 - bun add @hey-api/client-fetch && bun add @hey-api/openapi-ts -D 151 + bun add @hey-api/openapi-ts -D 155 152 ``` 156 153 157 154 We recommend pinning an exact version so you can safely upgrade when you're ready. This package is in [initial development](https://semver.org/spec/v0.1.0.html#spec-item-5) and its API might change before v1. ··· 178 175 createClient({ 179 176 input: 'https://get.heyapi.dev/hey-api/backend', 180 177 output: 'src/client', 181 - plugins: ['@hey-api/client-fetch'], 182 178 }); 183 179 ``` 184 180 ··· 194 190 export default defineConfig({ 195 191 input: 'https://get.heyapi.dev/hey-api/backend', 196 192 output: 'src/client', 197 - plugins: ['@hey-api/client-fetch'], 198 193 }); 199 194 ``` 200 195 ··· 205 200 module.exports = { 206 201 input: 'https://get.heyapi.dev/hey-api/backend', 207 202 output: 'src/client', 208 - plugins: ['@hey-api/client-fetch'], 209 203 }; 210 204 ``` 211 205 ··· 216 210 export default { 217 211 input: 'https://get.heyapi.dev/hey-api/backend', 218 212 output: 'src/client', 219 - plugins: ['@hey-api/client-fetch'], 220 213 }; 221 214 ``` 222 215
-1
packages/openapi-ts/package.json
··· 101 101 }, 102 102 "devDependencies": { 103 103 "@config/vite-base": "workspace:*", 104 - "@hey-api/client-core": "workspace:*", 105 104 "@types/cross-spawn": "6.0.6", 106 105 "@types/express": "4.17.21", 107 106 "axios": "1.8.2",
+50 -423
pnpm-lock.yaml
··· 20 20 '@config/vite-base': 21 21 specifier: workspace:* 22 22 version: link:packages/config-vite-base 23 - '@hey-api/client-axios': 24 - specifier: workspace:* 25 - version: link:packages/client-axios 26 23 '@hey-api/client-custom': 27 24 specifier: workspace:* 28 25 version: link:packages/client-custom 29 - '@hey-api/client-fetch': 30 - specifier: workspace:* 31 - version: link:packages/client-fetch 32 - '@hey-api/client-next': 33 - specifier: workspace:* 34 - version: link:packages/client-next 35 26 '@hey-api/client-nuxt': 36 27 specifier: workspace:* 37 28 version: link:packages/client-nuxt ··· 117 108 118 109 examples/openapi-ts-axios: 119 110 dependencies: 120 - '@hey-api/client-axios': 121 - specifier: workspace:* 122 - version: link:../../packages/client-axios 123 111 '@radix-ui/react-form': 124 112 specifier: 0.1.1 125 113 version: 0.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) ··· 190 178 191 179 examples/openapi-ts-fastify: 192 180 dependencies: 193 - '@hey-api/client-fetch': 194 - specifier: workspace:* 195 - version: link:../../packages/client-fetch 196 181 fastify: 197 182 specifier: 5.2.0 198 183 version: 5.2.0 ··· 224 209 225 210 examples/openapi-ts-fetch: 226 211 dependencies: 227 - '@hey-api/client-fetch': 228 - specifier: workspace:* 229 - version: link:../../packages/client-fetch 230 212 '@radix-ui/react-form': 231 213 specifier: 0.1.1 232 214 version: 0.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) ··· 294 276 295 277 examples/openapi-ts-next: 296 278 dependencies: 297 - '@hey-api/client-next': 298 - specifier: workspace:* 299 - version: link:../../packages/client-next 300 279 next: 301 280 specifier: 15.2.4 302 281 version: 15.2.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.0) ··· 359 338 360 339 examples/openapi-ts-sample: 361 340 dependencies: 362 - '@hey-api/client-fetch': 363 - specifier: workspace:* 364 - version: link:../../packages/client-fetch 365 341 '@radix-ui/react-form': 366 342 specifier: 0.1.1 367 343 version: 0.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) ··· 459 435 '@angular/router': 460 436 specifier: ^19.2.0 461 437 version: 19.2.0(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.2.0(@angular/animations@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(rxjs@7.8.1) 462 - '@hey-api/client-fetch': 463 - specifier: workspace:* 464 - version: link:../../packages/client-fetch 465 438 '@tanstack/angular-query-experimental': 466 439 specifier: 5.73.3 467 440 version: 5.73.3(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)) ··· 514 487 515 488 examples/openapi-ts-tanstack-react-query: 516 489 dependencies: 517 - '@hey-api/client-fetch': 518 - specifier: workspace:* 519 - version: link:../../packages/client-fetch 520 490 '@radix-ui/react-form': 521 491 specifier: 0.1.1 522 492 version: 0.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) ··· 590 560 591 561 examples/openapi-ts-tanstack-svelte-query: 592 562 dependencies: 593 - '@hey-api/client-fetch': 594 - specifier: workspace:* 595 - version: link:../../packages/client-fetch 596 563 '@tanstack/svelte-query': 597 564 specifier: 5.73.3 598 565 version: 5.73.3(svelte@5.19.9) ··· 660 627 661 628 examples/openapi-ts-tanstack-vue-query: 662 629 dependencies: 663 - '@hey-api/client-fetch': 664 - specifier: workspace:* 665 - version: link:../../packages/client-fetch 666 630 '@tanstack/vue-query': 667 631 specifier: 5.73.3 668 632 version: 5.73.3(vue@3.5.13(typescript@5.8.3)) ··· 755 719 specifier: 2.2.0 756 720 version: 2.2.0(typescript@5.8.3) 757 721 758 - packages/client-axios: 759 - devDependencies: 760 - '@config/vite-base': 761 - specifier: workspace:* 762 - version: link:../config-vite-base 763 - '@hey-api/client-core': 764 - specifier: workspace:* 765 - version: link:../client-core 766 - '@hey-api/openapi-ts': 767 - specifier: workspace:* 768 - version: link:../openapi-ts 769 - axios: 770 - specifier: 1.8.2 771 - version: 1.8.2 772 - 773 - packages/client-core: 774 - devDependencies: 775 - '@config/vite-base': 776 - specifier: workspace:* 777 - version: link:../config-vite-base 778 - 779 722 packages/client-custom: 780 723 devDependencies: 781 724 '@config/vite-base': 782 725 specifier: workspace:* 783 726 version: link:../config-vite-base 784 - '@hey-api/client-core': 785 - specifier: workspace:* 786 - version: link:../client-core 787 - '@hey-api/openapi-ts': 788 - specifier: workspace:* 789 - version: link:../openapi-ts 790 - 791 - packages/client-fetch: 792 - devDependencies: 793 - '@config/vite-base': 794 - specifier: workspace:* 795 - version: link:../config-vite-base 796 - '@hey-api/client-core': 797 - specifier: workspace:* 798 - version: link:../client-core 799 - '@hey-api/openapi-ts': 800 - specifier: workspace:* 801 - version: link:../openapi-ts 802 - 803 - packages/client-next: 804 - devDependencies: 805 - '@config/vite-base': 806 - specifier: workspace:* 807 - version: link:../config-vite-base 808 - '@hey-api/client-core': 809 - specifier: workspace:* 810 - version: link:../client-core 811 727 '@hey-api/openapi-ts': 812 728 specifier: workspace:* 813 729 version: link:../openapi-ts ··· 824 740 '@config/vite-base': 825 741 specifier: workspace:* 826 742 version: link:../config-vite-base 827 - '@hey-api/client-core': 828 - specifier: workspace:* 829 - version: link:../client-core 830 743 '@hey-api/openapi-ts': 831 744 specifier: workspace:* 832 745 version: link:../openapi-ts ··· 920 833 '@config/vite-base': 921 834 specifier: workspace:* 922 835 version: link:../config-vite-base 923 - '@hey-api/client-core': 924 - specifier: workspace:* 925 - version: link:../client-core 926 836 '@types/cross-spawn': 927 837 specifier: 6.0.6 928 838 version: 6.0.6 ··· 967 877 devDependencies: 968 878 '@angular-devkit/build-angular': 969 879 specifier: 19.2.0 970 - version: 19.2.0(@angular/compiler-cli@19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.8.3))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.4.2)(karma@6.4.4)(tailwindcss@3.4.9(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.8.3)))(typescript@5.8.3)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0))(yaml@2.8.0) 880 + version: 19.2.0(@angular/compiler-cli@19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.8.3))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.4.2)(karma@6.4.4)(tailwindcss@3.4.9(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.8.3)))(typescript@5.8.3)(vite@6.2.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(yaml@2.8.0) 971 881 '@angular/animations': 972 882 specifier: 19.2.0 973 883 version: 19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)) ··· 1054 964 version: 3.3.2 1055 965 nuxt: 1056 966 specifier: 3.14.1592 1057 - version: 3.14.1592(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.1)(encoding@0.1.13)(eslint@9.17.0(jiti@2.4.2))(ioredis@5.6.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.41.1)(sass@1.85.0)(terser@5.39.0)(typescript@5.8.3)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)) 967 + version: 3.14.1592(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.1)(encoding@0.1.13)(eslint@9.17.0(jiti@2.4.2))(ioredis@5.6.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.41.1)(sass@1.85.0)(terser@5.39.0)(typescript@5.8.3)(vite@6.2.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) 1058 968 prettier: 1059 969 specifier: 3.4.2 1060 970 version: 3.4.2 ··· 12266 12176 transitivePeerDependencies: 12267 12177 - chokidar 12268 12178 12269 - '@angular-devkit/build-angular@19.2.0(@angular/compiler-cli@19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.8.3))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.4.2)(karma@6.4.4)(tailwindcss@3.4.9(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.8.3)))(typescript@5.8.3)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0))(yaml@2.8.0)': 12179 + '@angular-devkit/build-angular@19.2.0(@angular/compiler-cli@19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.8.3))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.4.2)(karma@6.4.4)(tailwindcss@3.4.9(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.8.3)))(typescript@5.8.3)(vite@6.1.0(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(yaml@2.8.0)': 12270 12180 dependencies: 12271 12181 '@ampproject/remapping': 2.3.0 12272 12182 '@angular-devkit/architect': 0.1902.0(chokidar@4.0.3) 12273 - '@angular-devkit/build-webpack': 0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0))(webpack@5.98.0(esbuild@0.25.0)) 12183 + '@angular-devkit/build-webpack': 0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.0)))(webpack@5.98.0(esbuild@0.25.0)) 12274 12184 '@angular-devkit/core': 19.2.0(chokidar@4.0.3) 12275 12185 '@angular/build': 19.2.0(@angular/compiler-cli@19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.8.3))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.4.2)(karma@6.4.4)(less@4.2.2)(postcss@8.5.2)(tailwindcss@3.4.9(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.8.3)))(terser@5.39.0)(typescript@5.8.3)(yaml@2.8.0) 12276 12186 '@angular/compiler-cli': 19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.8.3) ··· 12285 12195 '@babel/runtime': 7.26.9 12286 12196 '@discoveryjs/json-ext': 0.6.3 12287 12197 '@ngtools/webpack': 19.2.0(@angular/compiler-cli@19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.8.3))(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)) 12288 - '@vitejs/plugin-basic-ssl': 1.2.0(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)) 12198 + '@vitejs/plugin-basic-ssl': 1.2.0(vite@6.1.0(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) 12289 12199 ansi-colors: 4.1.3 12290 12200 autoprefixer: 10.4.20(postcss@8.5.2) 12291 12201 babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0(esbuild@0.25.0)) ··· 12320 12230 tree-kill: 1.2.2 12321 12231 tslib: 2.8.1 12322 12232 typescript: 5.8.3 12323 - webpack: 5.98.0(esbuild@0.25.0) 12324 - webpack-dev-middleware: 7.4.2(webpack@5.98.0) 12325 - webpack-dev-server: 5.2.0(webpack@5.98.0) 12233 + webpack: 5.98.0(esbuild@0.25.2) 12234 + webpack-dev-middleware: 7.4.2(webpack@5.98.0(esbuild@0.25.0)) 12235 + webpack-dev-server: 5.2.0(webpack@5.98.0(esbuild@0.25.0)) 12326 12236 webpack-merge: 6.0.1 12327 12237 webpack-subresource-integrity: 5.1.0(webpack@5.98.0(esbuild@0.25.0)) 12328 12238 optionalDependencies: ··· 12352 12262 - webpack-cli 12353 12263 - yaml 12354 12264 12355 - '@angular-devkit/build-angular@19.2.0(@angular/compiler-cli@19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.8.3))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.4.2)(karma@6.4.4)(tailwindcss@3.4.9(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.8.3)))(typescript@5.8.3)(vite@6.1.0(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(yaml@2.8.0)': 12265 + '@angular-devkit/build-angular@19.2.0(@angular/compiler-cli@19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.8.3))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.4.2)(karma@6.4.4)(tailwindcss@3.4.9(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.8.3)))(typescript@5.8.3)(vite@6.2.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(yaml@2.8.0)': 12356 12266 dependencies: 12357 12267 '@ampproject/remapping': 2.3.0 12358 12268 '@angular-devkit/architect': 0.1902.0(chokidar@4.0.3) 12359 - '@angular-devkit/build-webpack': 0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.2)))(webpack@5.98.0(esbuild@0.25.0)) 12269 + '@angular-devkit/build-webpack': 0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.0)))(webpack@5.98.0(esbuild@0.25.0)) 12360 12270 '@angular-devkit/core': 19.2.0(chokidar@4.0.3) 12361 12271 '@angular/build': 19.2.0(@angular/compiler-cli@19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.8.3))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.4.2)(karma@6.4.4)(less@4.2.2)(postcss@8.5.2)(tailwindcss@3.4.9(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.8.3)))(terser@5.39.0)(typescript@5.8.3)(yaml@2.8.0) 12362 12272 '@angular/compiler-cli': 19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.8.3) ··· 12371 12281 '@babel/runtime': 7.26.9 12372 12282 '@discoveryjs/json-ext': 0.6.3 12373 12283 '@ngtools/webpack': 19.2.0(@angular/compiler-cli@19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.8.3))(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)) 12374 - '@vitejs/plugin-basic-ssl': 1.2.0(vite@6.1.0(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) 12284 + '@vitejs/plugin-basic-ssl': 1.2.0(vite@6.2.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) 12375 12285 ansi-colors: 4.1.3 12376 12286 autoprefixer: 10.4.20(postcss@8.5.2) 12377 12287 babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0(esbuild@0.25.0)) ··· 12406 12316 tree-kill: 1.2.2 12407 12317 tslib: 2.8.1 12408 12318 typescript: 5.8.3 12409 - webpack: 5.98.0(esbuild@0.25.0) 12410 - webpack-dev-middleware: 7.4.2(webpack@5.98.0(esbuild@0.25.2)) 12411 - webpack-dev-server: 5.2.0(webpack@5.98.0(esbuild@0.25.2)) 12319 + webpack: 5.98.0(esbuild@0.25.2) 12320 + webpack-dev-middleware: 7.4.2(webpack@5.98.0(esbuild@0.25.0)) 12321 + webpack-dev-server: 5.2.0(webpack@5.98.0(esbuild@0.25.0)) 12412 12322 webpack-merge: 6.0.1 12413 12323 webpack-subresource-integrity: 5.1.0(webpack@5.98.0(esbuild@0.25.0)) 12414 12324 optionalDependencies: ··· 12438 12348 - webpack-cli 12439 12349 - yaml 12440 12350 12441 - '@angular-devkit/build-webpack@0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.2)))(webpack@5.98.0(esbuild@0.25.0))': 12351 + '@angular-devkit/build-webpack@0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.0)))(webpack@5.98.0(esbuild@0.25.0))': 12442 12352 dependencies: 12443 12353 '@angular-devkit/architect': 0.1902.0(chokidar@4.0.3) 12444 12354 rxjs: 7.8.1 12445 - webpack: 5.98.0(esbuild@0.25.0) 12446 - webpack-dev-server: 5.2.0(webpack@5.98.0(esbuild@0.25.2)) 12447 - transitivePeerDependencies: 12448 - - chokidar 12449 - 12450 - '@angular-devkit/build-webpack@0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0))(webpack@5.98.0(esbuild@0.25.0))': 12451 - dependencies: 12452 - '@angular-devkit/architect': 0.1902.0(chokidar@4.0.3) 12453 - rxjs: 7.8.1 12454 - webpack: 5.98.0(esbuild@0.25.0) 12455 - webpack-dev-server: 5.2.0(webpack@5.98.0) 12355 + webpack: 5.98.0(esbuild@0.25.2) 12356 + webpack-dev-server: 5.2.0(webpack@5.98.0(esbuild@0.25.0)) 12456 12357 transitivePeerDependencies: 12457 12358 - chokidar 12458 12359 ··· 14647 14548 dependencies: 14648 14549 '@angular/compiler-cli': 19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.8.3) 14649 14550 typescript: 5.8.3 14650 - webpack: 5.98.0(esbuild@0.25.0) 14551 + webpack: 5.98.0(esbuild@0.25.2) 14651 14552 14652 14553 '@nodelib/fs.scandir@2.1.5': 14653 14554 dependencies: ··· 14724 14625 14725 14626 '@nuxt/devalue@2.0.2': {} 14726 14627 14727 - '@nuxt/devtools-kit@1.7.0(magicast@0.3.5)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0))': 14728 - dependencies: 14729 - '@nuxt/kit': 3.15.4(magicast@0.3.5) 14730 - '@nuxt/schema': 3.16.2 14731 - execa: 7.2.0 14732 - vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0) 14733 - transitivePeerDependencies: 14734 - - magicast 14735 - - supports-color 14736 - 14737 14628 '@nuxt/devtools-kit@1.7.0(magicast@0.3.5)(vite@6.2.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))': 14738 14629 dependencies: 14739 14630 '@nuxt/kit': 3.15.4(magicast@0.3.5) ··· 14804 14695 - utf-8-validate 14805 14696 - vue 14806 14697 14807 - '@nuxt/devtools@1.7.0(rollup@4.41.1)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0))(vue@3.5.13(typescript@5.8.3))': 14808 - dependencies: 14809 - '@antfu/utils': 0.7.10 14810 - '@nuxt/devtools-kit': 1.7.0(magicast@0.3.5)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)) 14811 - '@nuxt/devtools-wizard': 1.7.0 14812 - '@nuxt/kit': 3.15.4(magicast@0.3.5) 14813 - '@vue/devtools-core': 7.6.8(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0))(vue@3.5.13(typescript@5.8.3)) 14814 - '@vue/devtools-kit': 7.6.8 14815 - birpc: 0.2.19 14816 - consola: 3.4.0 14817 - cronstrue: 2.56.0 14818 - destr: 2.0.3 14819 - error-stack-parser-es: 0.1.5 14820 - execa: 7.2.0 14821 - fast-npm-meta: 0.2.2 14822 - flatted: 3.3.3 14823 - get-port-please: 3.1.2 14824 - hookable: 5.5.3 14825 - image-meta: 0.2.1 14826 - is-installed-globally: 1.0.0 14827 - launch-editor: 2.10.0 14828 - local-pkg: 0.5.1 14829 - magicast: 0.3.5 14830 - nypm: 0.4.1 14831 - ohash: 1.1.6 14832 - pathe: 1.1.2 14833 - perfect-debounce: 1.0.0 14834 - pkg-types: 1.3.1 14835 - rc9: 2.1.2 14836 - scule: 1.3.0 14837 - semver: 7.7.1 14838 - simple-git: 3.27.0 14839 - sirv: 3.0.1 14840 - tinyglobby: 0.2.12 14841 - unimport: 3.14.6(rollup@4.41.1) 14842 - vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0) 14843 - vite-plugin-inspect: 0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.41.1)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)) 14844 - vite-plugin-vue-inspector: 5.3.1(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)) 14845 - which: 3.0.1 14846 - ws: 8.18.1 14847 - transitivePeerDependencies: 14848 - - bufferutil 14849 - - rollup 14850 - - supports-color 14851 - - utf-8-validate 14852 - - vue 14853 - 14854 14698 '@nuxt/devtools@1.7.0(rollup@4.41.1)(vite@6.2.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(vue@3.5.13(typescript@5.8.3))': 14855 14699 dependencies: 14856 14700 '@antfu/utils': 0.7.10 ··· 16983 16827 - rollup 16984 16828 - supports-color 16985 16829 16986 - '@vitejs/plugin-basic-ssl@1.2.0(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0))': 16987 - dependencies: 16988 - vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0) 16989 - 16990 16830 '@vitejs/plugin-basic-ssl@1.2.0(vite@6.1.0(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))': 16991 16831 dependencies: 16992 16832 vite: 6.1.0(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0) 16833 + 16834 + '@vitejs/plugin-basic-ssl@1.2.0(vite@6.2.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))': 16835 + dependencies: 16836 + vite: 6.2.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0) 16993 16837 16994 16838 '@vitejs/plugin-react@4.4.0-beta.1(vite@6.2.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))': 16995 16839 dependencies: ··· 17188 17032 dependencies: 17189 17033 '@vue/devtools-kit': 7.7.2 17190 17034 17191 - '@vue/devtools-core@7.6.8(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0))(vue@3.5.13(typescript@5.8.3))': 17192 - dependencies: 17193 - '@vue/devtools-kit': 7.7.2 17194 - '@vue/devtools-shared': 7.7.2 17195 - mitt: 3.0.1 17196 - nanoid: 5.1.5 17197 - pathe: 1.1.2 17198 - vite-hot-client: 0.2.4(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)) 17199 - vue: 3.5.13(typescript@5.8.3) 17200 - transitivePeerDependencies: 17201 - - vite 17202 - 17203 17035 '@vue/devtools-core@7.6.8(vite@6.2.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(vue@3.5.13(typescript@5.8.3))': 17204 17036 dependencies: 17205 17037 '@vue/devtools-kit': 7.7.2 ··· 17755 17587 '@babel/core': 7.26.9 17756 17588 find-cache-dir: 4.0.0 17757 17589 schema-utils: 4.3.0 17758 - webpack: 5.98.0(esbuild@0.25.0) 17590 + webpack: 5.98.0(esbuild@0.25.2) 17759 17591 17760 17592 babel-plugin-polyfill-corejs2@0.4.12(@babel/core@7.26.9): 17761 17593 dependencies: ··· 18289 18121 normalize-path: 3.0.0 18290 18122 schema-utils: 4.3.0 18291 18123 serialize-javascript: 6.0.2 18292 - webpack: 5.98.0(esbuild@0.25.0) 18124 + webpack: 5.98.0(esbuild@0.25.2) 18293 18125 18294 18126 core-js-compat@3.41.0: 18295 18127 dependencies: ··· 18361 18193 postcss-value-parser: 4.2.0 18362 18194 semver: 7.7.1 18363 18195 optionalDependencies: 18364 - webpack: 5.98.0(esbuild@0.25.0) 18196 + webpack: 5.98.0(esbuild@0.25.2) 18365 18197 18366 18198 css-select@5.1.0: 18367 18199 dependencies: ··· 19029 18861 '@typescript-eslint/parser': 8.29.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.8.3) 19030 18862 eslint: 9.17.0(jiti@2.4.2) 19031 18863 eslint-import-resolver-node: 0.3.9 19032 - eslint-import-resolver-typescript: 3.8.5(eslint-plugin-import@2.31.0)(eslint@9.17.0(jiti@2.4.2)) 19033 - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.5)(eslint@9.17.0(jiti@2.4.2)) 18864 + eslint-import-resolver-typescript: 3.8.5(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2)) 18865 + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.5(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2)) 19034 18866 eslint-plugin-jsx-a11y: 6.10.2(eslint@9.17.0(jiti@2.4.2)) 19035 18867 eslint-plugin-react: 7.37.4(eslint@9.17.0(jiti@2.4.2)) 19036 18868 eslint-plugin-react-hooks: 5.2.0(eslint@9.17.0(jiti@2.4.2)) ··· 19053 18885 transitivePeerDependencies: 19054 18886 - supports-color 19055 18887 19056 - eslint-import-resolver-typescript@3.8.5(eslint-plugin-import@2.31.0)(eslint@9.17.0(jiti@2.4.2)): 18888 + eslint-import-resolver-typescript@3.8.5(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2)): 19057 18889 dependencies: 19058 18890 '@nolyfill/is-core-module': 1.0.39 19059 18891 debug: 4.4.0(supports-color@9.4.0) ··· 19064 18896 stable-hash: 0.0.4 19065 18897 tinyglobby: 0.2.12 19066 18898 optionalDependencies: 19067 - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.5)(eslint@9.17.0(jiti@2.4.2)) 18899 + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.5(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2)) 19068 18900 transitivePeerDependencies: 19069 18901 - supports-color 19070 18902 19071 - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.5)(eslint@9.17.0(jiti@2.4.2)): 18903 + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.5(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2)): 19072 18904 dependencies: 19073 18905 debug: 3.2.7 19074 18906 optionalDependencies: 19075 18907 '@typescript-eslint/parser': 8.29.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.8.3) 19076 18908 eslint: 9.17.0(jiti@2.4.2) 19077 18909 eslint-import-resolver-node: 0.3.9 19078 - eslint-import-resolver-typescript: 3.8.5(eslint-plugin-import@2.31.0)(eslint@9.17.0(jiti@2.4.2)) 18910 + eslint-import-resolver-typescript: 3.8.5(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2)) 19079 18911 transitivePeerDependencies: 19080 18912 - supports-color 19081 18913 19082 - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.5)(eslint@9.17.0(jiti@2.4.2)): 18914 + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.5(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2)): 19083 18915 dependencies: 19084 18916 '@rtsao/scc': 1.1.0 19085 18917 array-includes: 3.1.8 ··· 19090 18922 doctrine: 2.1.0 19091 18923 eslint: 9.17.0(jiti@2.4.2) 19092 18924 eslint-import-resolver-node: 0.3.9 19093 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.5)(eslint@9.17.0(jiti@2.4.2)) 18925 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.5(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2)))(eslint@9.17.0(jiti@2.4.2)) 19094 18926 hasown: 2.0.2 19095 18927 is-core-module: 2.16.1 19096 18928 is-glob: 4.0.3 ··· 20716 20548 dependencies: 20717 20549 less: 4.2.2 20718 20550 optionalDependencies: 20719 - webpack: 5.98.0(esbuild@0.25.0) 20551 + webpack: 5.98.0(esbuild@0.25.2) 20720 20552 20721 20553 less@4.2.2: 20722 20554 dependencies: ··· 20741 20573 dependencies: 20742 20574 webpack-sources: 3.2.3 20743 20575 optionalDependencies: 20744 - webpack: 5.98.0(esbuild@0.25.0) 20576 + webpack: 5.98.0(esbuild@0.25.2) 20745 20577 20746 20578 light-my-request@6.6.0: 20747 20579 dependencies: ··· 21069 20901 dependencies: 21070 20902 schema-utils: 4.3.0 21071 20903 tapable: 2.2.1 21072 - webpack: 5.98.0(esbuild@0.25.0) 20904 + webpack: 5.98.0(esbuild@0.25.2) 21073 20905 21074 20906 minimalistic-assert@1.0.1: {} 21075 20907 ··· 21637 21469 - vue-tsc 21638 21470 - xml2js 21639 21471 21640 - nuxt@3.14.1592(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.1)(encoding@0.1.13)(eslint@9.17.0(jiti@2.4.2))(ioredis@5.6.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.41.1)(sass@1.85.0)(terser@5.39.0)(typescript@5.8.3)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)): 21641 - dependencies: 21642 - '@nuxt/devalue': 2.0.2 21643 - '@nuxt/devtools': 1.7.0(rollup@4.41.1)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0))(vue@3.5.13(typescript@5.8.3)) 21644 - '@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.41.1) 21645 - '@nuxt/schema': 3.14.1592(magicast@0.3.5)(rollup@4.41.1) 21646 - '@nuxt/telemetry': 2.6.5(magicast@0.3.5) 21647 - '@nuxt/vite-builder': 3.14.1592(@types/node@22.10.5)(eslint@9.17.0(jiti@2.4.2))(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.41.1)(sass@1.85.0)(terser@5.39.0)(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)) 21648 - '@unhead/dom': 1.11.20 21649 - '@unhead/shared': 1.11.20 21650 - '@unhead/ssr': 1.11.20 21651 - '@unhead/vue': 1.11.20(vue@3.5.13(typescript@5.8.3)) 21652 - '@vue/shared': 3.5.13 21653 - acorn: 8.14.0 21654 - c12: 2.0.1(magicast@0.3.5) 21655 - chokidar: 4.0.3 21656 - compatx: 0.1.8 21657 - consola: 3.4.0 21658 - cookie-es: 1.2.2 21659 - defu: 6.1.4 21660 - destr: 2.0.3 21661 - devalue: 5.1.1 21662 - errx: 0.1.0 21663 - esbuild: 0.24.2 21664 - escape-string-regexp: 5.0.0 21665 - estree-walker: 3.0.3 21666 - globby: 14.1.0 21667 - h3: 1.15.1 21668 - hookable: 5.5.3 21669 - ignore: 6.0.2 21670 - impound: 0.2.2(rollup@4.41.1) 21671 - jiti: 2.4.2 21672 - klona: 2.0.6 21673 - knitwork: 1.2.0 21674 - magic-string: 0.30.17 21675 - mlly: 1.7.4 21676 - nanotar: 0.1.1 21677 - nitropack: 2.11.6(encoding@0.1.13)(typescript@5.8.3) 21678 - nuxi: 3.22.5 21679 - nypm: 0.3.12 21680 - ofetch: 1.4.1 21681 - ohash: 1.1.6 21682 - pathe: 1.1.2 21683 - perfect-debounce: 1.0.0 21684 - pkg-types: 1.3.1 21685 - radix3: 1.1.2 21686 - scule: 1.3.0 21687 - semver: 7.7.1 21688 - std-env: 3.8.1 21689 - strip-literal: 2.1.1 21690 - tinyglobby: 0.2.10 21691 - ufo: 1.5.4 21692 - ultrahtml: 1.5.3 21693 - uncrypto: 0.1.3 21694 - unctx: 2.4.1 21695 - unenv: 1.10.0 21696 - unhead: 1.11.20 21697 - unimport: 3.14.6(rollup@4.41.1) 21698 - unplugin: 1.16.1 21699 - unplugin-vue-router: 0.10.9(rollup@4.41.1)(vue-router@4.5.0(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3)) 21700 - unstorage: 1.15.0(db0@0.3.1)(ioredis@5.6.0) 21701 - untyped: 1.5.2 21702 - vue: 3.5.13(typescript@5.8.3) 21703 - vue-bundle-renderer: 2.1.1 21704 - vue-devtools-stub: 0.1.0 21705 - vue-router: 4.5.0(vue@3.5.13(typescript@5.8.3)) 21706 - optionalDependencies: 21707 - '@parcel/watcher': 2.5.1 21708 - '@types/node': 22.10.5 21709 - transitivePeerDependencies: 21710 - - '@azure/app-configuration' 21711 - - '@azure/cosmos' 21712 - - '@azure/data-tables' 21713 - - '@azure/identity' 21714 - - '@azure/keyvault-secrets' 21715 - - '@azure/storage-blob' 21716 - - '@biomejs/biome' 21717 - - '@capacitor/preferences' 21718 - - '@deno/kv' 21719 - - '@electric-sql/pglite' 21720 - - '@libsql/client' 21721 - - '@netlify/blobs' 21722 - - '@planetscale/database' 21723 - - '@upstash/redis' 21724 - - '@vercel/blob' 21725 - - '@vercel/kv' 21726 - - aws4fetch 21727 - - better-sqlite3 21728 - - bufferutil 21729 - - db0 21730 - - drizzle-orm 21731 - - encoding 21732 - - eslint 21733 - - idb-keyval 21734 - - ioredis 21735 - - less 21736 - - lightningcss 21737 - - magicast 21738 - - meow 21739 - - mysql2 21740 - - optionator 21741 - - rolldown 21742 - - rollup 21743 - - sass 21744 - - sass-embedded 21745 - - sqlite3 21746 - - stylelint 21747 - - stylus 21748 - - sugarss 21749 - - supports-color 21750 - - terser 21751 - - typescript 21752 - - uploadthing 21753 - - utf-8-validate 21754 - - vite 21755 - - vls 21756 - - vti 21757 - - vue-tsc 21758 - - xml2js 21759 - 21760 21472 nuxt@3.14.1592(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.1)(encoding@0.1.13)(eslint@9.17.0(jiti@2.4.2))(ioredis@5.6.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.41.1)(sass@1.85.0)(terser@5.39.0)(typescript@5.8.3)(vite@6.2.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)): 21761 21473 dependencies: 21762 21474 '@nuxt/devalue': 2.0.2 ··· 22399 22111 postcss: 8.5.2 22400 22112 semver: 7.7.1 22401 22113 optionalDependencies: 22402 - webpack: 5.98.0(esbuild@0.25.0) 22114 + webpack: 5.98.0(esbuild@0.25.2) 22403 22115 transitivePeerDependencies: 22404 22116 - typescript 22405 22117 ··· 23162 22874 neo-async: 2.6.2 23163 22875 optionalDependencies: 23164 22876 sass: 1.85.0 23165 - webpack: 5.98.0(esbuild@0.25.0) 22877 + webpack: 5.98.0(esbuild@0.25.2) 23166 22878 23167 22879 sass@1.85.0: 23168 22880 dependencies: ··· 23502 23214 dependencies: 23503 23215 iconv-lite: 0.6.3 23504 23216 source-map-js: 1.2.1 23505 - webpack: 5.98.0(esbuild@0.25.0) 23217 + webpack: 5.98.0(esbuild@0.25.2) 23506 23218 23507 23219 source-map-support@0.5.21: 23508 23220 dependencies: ··· 23883 23595 23884 23596 term-size@2.2.1: {} 23885 23597 23886 - terser-webpack-plugin@5.3.14(esbuild@0.25.0)(webpack@5.98.0): 23598 + terser-webpack-plugin@5.3.14(esbuild@0.25.2)(webpack@5.98.0(esbuild@0.25.0)): 23887 23599 dependencies: 23888 23600 '@jridgewell/trace-mapping': 0.3.25 23889 23601 jest-worker: 27.5.1 23890 23602 schema-utils: 4.3.0 23891 23603 serialize-javascript: 6.0.2 23892 23604 terser: 5.39.0 23893 - webpack: 5.98.0(esbuild@0.25.0) 23605 + webpack: 5.98.0(esbuild@0.25.2) 23894 23606 optionalDependencies: 23895 - esbuild: 0.25.0 23607 + esbuild: 0.25.2 23896 23608 23897 23609 terser@5.39.0: 23898 23610 dependencies: ··· 24571 24283 '@types/unist': 3.0.3 24572 24284 vfile-message: 4.0.2 24573 24285 24574 - vite-hot-client@0.2.4(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)): 24575 - dependencies: 24576 - vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0) 24577 - 24578 24286 vite-hot-client@0.2.4(vite@6.2.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)): 24579 24287 dependencies: 24580 24288 vite: 6.2.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0) ··· 24658 24366 - rollup 24659 24367 - supports-color 24660 24368 24661 - vite-plugin-inspect@0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.41.1)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)): 24662 - dependencies: 24663 - '@antfu/utils': 0.7.10 24664 - '@rollup/pluginutils': 5.1.4(rollup@4.41.1) 24665 - debug: 4.4.0(supports-color@9.4.0) 24666 - error-stack-parser-es: 0.1.5 24667 - fs-extra: 11.3.0 24668 - open: 10.1.0 24669 - perfect-debounce: 1.0.0 24670 - picocolors: 1.1.1 24671 - sirv: 3.0.1 24672 - vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0) 24673 - optionalDependencies: 24674 - '@nuxt/kit': 3.15.4(magicast@0.3.5) 24675 - transitivePeerDependencies: 24676 - - rollup 24677 - - supports-color 24678 - 24679 24369 vite-plugin-inspect@0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.41.1)(vite@6.2.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)): 24680 24370 dependencies: 24681 24371 '@antfu/utils': 0.7.10 ··· 24709 24399 - rollup 24710 24400 - supports-color 24711 24401 - vue 24712 - 24713 - vite-plugin-vue-inspector@5.3.1(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)): 24714 - dependencies: 24715 - '@babel/core': 7.26.10 24716 - '@babel/plugin-proposal-decorators': 7.25.9(@babel/core@7.26.10) 24717 - '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.10) 24718 - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.26.10) 24719 - '@babel/plugin-transform-typescript': 7.26.8(@babel/core@7.26.10) 24720 - '@vue/babel-plugin-jsx': 1.4.0(@babel/core@7.26.10) 24721 - '@vue/compiler-dom': 3.5.13 24722 - kolorist: 1.8.0 24723 - magic-string: 0.30.17 24724 - vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.39.0) 24725 - transitivePeerDependencies: 24726 - - supports-color 24727 24402 24728 24403 vite-plugin-vue-inspector@5.3.1(vite@6.2.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)): 24729 24404 dependencies: ··· 25010 24685 25011 24686 webidl-conversions@7.0.0: {} 25012 24687 25013 - webpack-dev-middleware@7.4.2(webpack@5.98.0(esbuild@0.25.2)): 25014 - dependencies: 25015 - colorette: 2.0.20 25016 - memfs: 4.17.0 25017 - mime-types: 2.1.35 25018 - on-finished: 2.4.1 25019 - range-parser: 1.2.1 25020 - schema-utils: 4.3.0 25021 - optionalDependencies: 25022 - webpack: 5.98.0(esbuild@0.25.0) 25023 - 25024 - webpack-dev-middleware@7.4.2(webpack@5.98.0): 24688 + webpack-dev-middleware@7.4.2(webpack@5.98.0(esbuild@0.25.0)): 25025 24689 dependencies: 25026 24690 colorette: 2.0.20 25027 24691 memfs: 4.17.0 ··· 25030 24694 range-parser: 1.2.1 25031 24695 schema-utils: 4.3.0 25032 24696 optionalDependencies: 25033 - webpack: 5.98.0(esbuild@0.25.0) 24697 + webpack: 5.98.0(esbuild@0.25.2) 25034 24698 25035 - webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.2)): 24699 + webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.0)): 25036 24700 dependencies: 25037 24701 '@types/bonjour': 3.5.13 25038 24702 '@types/connect-history-api-fallback': 1.5.4 ··· 25059 24723 serve-index: 1.9.1 25060 24724 sockjs: 0.3.24 25061 24725 spdy: 4.0.2 25062 - webpack-dev-middleware: 7.4.2(webpack@5.98.0(esbuild@0.25.2)) 24726 + webpack-dev-middleware: 7.4.2(webpack@5.98.0(esbuild@0.25.0)) 25063 24727 ws: 8.18.1 25064 24728 optionalDependencies: 25065 - webpack: 5.98.0(esbuild@0.25.0) 25066 - transitivePeerDependencies: 25067 - - bufferutil 25068 - - debug 25069 - - supports-color 25070 - - utf-8-validate 25071 - 25072 - webpack-dev-server@5.2.0(webpack@5.98.0): 25073 - dependencies: 25074 - '@types/bonjour': 3.5.13 25075 - '@types/connect-history-api-fallback': 1.5.4 25076 - '@types/express': 4.17.21 25077 - '@types/serve-index': 1.9.4 25078 - '@types/serve-static': 1.15.7 25079 - '@types/sockjs': 0.3.36 25080 - '@types/ws': 8.18.0 25081 - ansi-html-community: 0.0.8 25082 - bonjour-service: 1.3.0 25083 - chokidar: 3.6.0 25084 - colorette: 2.0.20 25085 - compression: 1.8.0 25086 - connect-history-api-fallback: 2.0.0 25087 - express: 4.21.2 25088 - graceful-fs: 4.2.11 25089 - http-proxy-middleware: 2.0.7(@types/express@4.17.21) 25090 - ipaddr.js: 2.2.0 25091 - launch-editor: 2.10.0 25092 - open: 10.1.0 25093 - p-retry: 6.2.1 25094 - schema-utils: 4.3.0 25095 - selfsigned: 2.4.1 25096 - serve-index: 1.9.1 25097 - sockjs: 0.3.24 25098 - spdy: 4.0.2 25099 - webpack-dev-middleware: 7.4.2(webpack@5.98.0) 25100 - ws: 8.18.1 25101 - optionalDependencies: 25102 - webpack: 5.98.0(esbuild@0.25.0) 24729 + webpack: 5.98.0(esbuild@0.25.2) 25103 24730 transitivePeerDependencies: 25104 24731 - bufferutil 25105 24732 - debug ··· 25117 24744 webpack-subresource-integrity@5.1.0(webpack@5.98.0(esbuild@0.25.0)): 25118 24745 dependencies: 25119 24746 typed-assert: 1.0.9 25120 - webpack: 5.98.0(esbuild@0.25.0) 24747 + webpack: 5.98.0(esbuild@0.25.2) 25121 24748 25122 24749 webpack-virtual-modules@0.6.2: {} 25123 24750 25124 - webpack@5.98.0(esbuild@0.25.0): 24751 + webpack@5.98.0(esbuild@0.25.2): 25125 24752 dependencies: 25126 24753 '@types/eslint-scope': 3.7.7 25127 24754 '@types/estree': 1.0.6 ··· 25143 24770 neo-async: 2.6.2 25144 24771 schema-utils: 4.3.0 25145 24772 tapable: 2.2.1 25146 - terser-webpack-plugin: 5.3.14(esbuild@0.25.0)(webpack@5.98.0) 24773 + terser-webpack-plugin: 5.3.14(esbuild@0.25.2)(webpack@5.98.0(esbuild@0.25.0)) 25147 24774 watchpack: 2.4.2 25148 24775 webpack-sources: 3.2.3 25149 24776 transitivePeerDependencies: