├── .npmignore ├── pnpm-workspace.yaml ├── src ├── index.ts ├── __test__ │ ├── query.test.ts │ ├── immutable.test.ts │ ├── infinite.test.ts │ ├── query-base.test.ts │ ├── mutate.test.ts │ ├── types.test-d.ts │ └── fixtures │ │ └── petstore.ts ├── query.ts ├── immutable.ts ├── types.ts ├── query-base.ts ├── infinite.ts └── mutate.ts ├── biome.json ├── docs ├── index.md ├── api │ ├── use-immutable.md │ ├── use-query.md │ ├── use-mutate.md │ ├── hook-builders.md │ └── use-infinite.md ├── .vitepress │ └── config.ts └── getting-started.md ├── tsconfig.json ├── LICENSE ├── .github └── workflows │ └── docs.yml ├── README.md ├── package.json ├── .gitignore └── CHANGELOG.md /.npmignore: -------------------------------------------------------------------------------- 1 | .turbo 2 | *.config.* 3 | biome.json 4 | tsconfig*.json 5 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - core-js 3 | - esbuild 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./immutable.js"; 2 | export * from "./infinite.js"; 3 | export * from "./mutate.js"; 4 | export * from "./query.js"; 5 | export * from "./types.js"; 6 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": false, 3 | "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", 4 | "extends": "//", 5 | "files": { 6 | "includes": ["src/**"] 7 | }, 8 | "linter": { 9 | "rules": { 10 | "style": { 11 | "noNonNullAssertion": "off" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/__test__/query.test.ts: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | import { describe, expect, it, vi } from "vitest"; 3 | import * as QueryBase from "../query-base.js"; 4 | 5 | vi.mock("../query-base.js"); 6 | const { configureBaseQueryHook } = vi.mocked(QueryBase); 7 | // @ts-expect-error - return type is not relevant to this test 8 | configureBaseQueryHook.mockReturnValue("pretend"); 9 | 10 | describe("createQueryHook", () => { 11 | it("creates factory function using useSWR", async () => { 12 | const { createQueryHook } = await import("../query.js"); 13 | 14 | expect(configureBaseQueryHook).toHaveBeenLastCalledWith(useSWR); 15 | expect(createQueryHook).toBe("pretend"); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/__test__/immutable.test.ts: -------------------------------------------------------------------------------- 1 | import useSWRImmutable from "swr/immutable"; 2 | import { describe, expect, it, vi } from "vitest"; 3 | import * as QueryBase from "../query-base.js"; 4 | 5 | vi.mock("../query-base.js"); 6 | const { configureBaseQueryHook } = vi.mocked(QueryBase); 7 | // @ts-expect-error - return type is not relevant to this test 8 | configureBaseQueryHook.mockReturnValue("pretend"); 9 | 10 | describe("createImmutableHook", () => { 11 | it("creates factory function using useSWRImmutable", async () => { 12 | const { createImmutableHook } = await import("../immutable.js"); 13 | 14 | expect(configureBaseQueryHook).toHaveBeenLastCalledWith(useSWRImmutable); 15 | expect(createImmutableHook).toBe("pretend"); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "SWR OpenAPI" 7 | tagline: Bindings for SWR and OpenAPI schemas 8 | actions: 9 | - theme: brand 10 | text: Getting Started 11 | link: /getting-started 12 | - theme: alt 13 | text: API Reference 14 | link: /api/hook-builders 15 | 16 | features: 17 | - title: Familiar API 18 | details: | 19 | First-class wrappers for hooks like useQuery, useMutate, and more 20 | - title: Type-safe 21 | details: Provides type safety for all requests to OpenAPI endpoints 22 | - title: Built on trusted tooling 23 | details: This package is a thin wrapper over openapi-typescript and openapi-fetch. 24 | --- 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | // From the excellent: https://www.totaltypescript.com/how-to-create-an-npm-package 2 | { 3 | "compilerOptions": { 4 | /* Base Options: */ 5 | "esModuleInterop": true, 6 | "skipLibCheck": true, 7 | "target": "es2022", 8 | "allowJs": true, 9 | "resolveJsonModule": true, 10 | "moduleDetection": "force", 11 | "isolatedModules": true, 12 | "verbatimModuleSyntax": true, 13 | 14 | /* Strictness */ 15 | "strict": true, 16 | "noUncheckedIndexedAccess": true, 17 | "noImplicitOverride": true, 18 | 19 | /* If transpiling with TypeScript: */ 20 | "module": "NodeNext", 21 | "moduleResolution": "nodenext", 22 | // "outDir": "dist", 23 | "sourceMap": true, 24 | 25 | /* AND if you're building for a library: */ 26 | "declaration": true, 27 | 28 | "noEmit": true 29 | }, 30 | "exclude": ["dist"] 31 | } 32 | -------------------------------------------------------------------------------- /src/query.ts: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | import { configureBaseQueryHook } from "./query-base.js"; 3 | 4 | /** 5 | * Produces a typed wrapper for [`useSWR`](https://swr.vercel.app/docs/api). 6 | * 7 | * ```ts 8 | * import createClient from "openapi-fetch"; 9 | * 10 | * const client = createClient(); 11 | * 12 | * const useQuery = createQueryHook(client, ""); 13 | * 14 | * // Fetch the query 15 | * useQuery("/pets"); 16 | * 17 | * // Skip the query 18 | * useQuery("/pets", null); 19 | * 20 | * // Fetch the query with parameters 21 | * useQuery("/pets", { 22 | * params: { query: { limit: 10 } } 23 | * }); 24 | * 25 | * // Fetch the query with parameters and SWR configuration 26 | * useQuery( 27 | * "/pets", 28 | * { params: { query: { limit: 10 } } }, 29 | * { errorRetryCount: 2 }, 30 | * ); 31 | * 32 | * // Fetch the query with no parameters and SWR configuration 33 | * useQuery( 34 | * "/pets", 35 | * {}, 36 | * { errorRetryCount: 2 }, 37 | * ); 38 | * ``` 39 | */ 40 | export const createQueryHook = configureBaseQueryHook(useSWR); 41 | -------------------------------------------------------------------------------- /docs/api/use-immutable.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useImmutable 3 | --- 4 | 5 | # {{ $frontmatter.title }} 6 | 7 | This hook has the same contracts as [`useQuery`](./use-query.md). However, instead of wrapping [`useSWR`][swr-api], it wraps `useSWRImmutable`. This immutable hook [disables automatic revalidations][swr-disable-auto-revalidate] but is otherwise identical to `useSWR`. 8 | 9 | ```ts 10 | import createClient from "openapi-fetch"; 11 | import { createImmutableHook } from "swr-openapi"; 12 | import type { paths } from "./my-schema"; 13 | 14 | const useImmutable = createImmutableHook(client, "my-api"); 15 | 16 | const { data, error, isLoading, isValidating, mutate } = useImmutable( 17 | path, 18 | init, 19 | config, 20 | ); 21 | ``` 22 | 23 | ## API 24 | 25 | ### Parameters 26 | 27 | Identical to `useQuery` [parameters](./use-query.md#parameters). 28 | 29 | ### Returns 30 | 31 | Identical to `useQuery` [returns](./use-query.md#returns). 32 | 33 | 34 | [swr-disable-auto-revalidate]: https://swr.vercel.app/docs/revalidation.en-US#disable-automatic-revalidations 35 | [swr-api]: https://swr.vercel.app/docs/api 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Hunter Tunnicliff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/immutable.ts: -------------------------------------------------------------------------------- 1 | import useSWRImmutable from "swr/immutable"; 2 | import { configureBaseQueryHook } from "./query-base.js"; 3 | 4 | /** 5 | * Produces a typed wrapper for [`useSWRImmutable`](https://swr.vercel.app/docs/revalidation.en-US#disable-automatic-revalidations). 6 | * 7 | * ```ts 8 | * import createClient from "openapi-fetch"; 9 | * const client = createClient(); 10 | * 11 | * const useImmutable = createImmutableHook(client, ""); 12 | * 13 | * // Fetch the query 14 | * useImmutable("/pets"); 15 | * 16 | * // Skip the query 17 | * useImmutable("/pets", null); 18 | * 19 | * // Fetch the query with parameters 20 | * useImmutable("/pets", { 21 | * params: { query: { limit: 10 } } 22 | * }); 23 | * 24 | * // Fetch the query with parameters and SWR configuration 25 | * useImmutable( 26 | * "/pets", 27 | * { params: { query: { limit: 10 } } }, 28 | * { errorRetryCount: 2 }, 29 | * ); 30 | * 31 | * // Fetch the query with no parameters and SWR configuration 32 | * useImmutable( 33 | * "/pets", 34 | * {}, 35 | * { errorRetryCount: 2 }, 36 | * ); 37 | * ``` 38 | */ 39 | export const createImmutableHook = configureBaseQueryHook(useSWRImmutable); 40 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy VitePress site to Pages 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | concurrency: 14 | group: pages 15 | cancel-in-progress: false 16 | 17 | jobs: 18 | build: 19 | name: Build 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v5 24 | 25 | - name: Setup pnpm 26 | uses: pnpm/action-setup@v4 27 | 28 | - name: Setup Node 29 | uses: actions/setup-node@v6 30 | with: 31 | node-version: 24 32 | cache: pnpm 33 | 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v4 36 | 37 | - name: Install dependencies 38 | run: pnpm install 39 | 40 | - name: Build with VitePress 41 | run: pnpm docs:build 42 | 43 | - name: Upload artifact 44 | uses: actions/upload-pages-artifact@v3 45 | with: 46 | path: docs/.vitepress/dist 47 | 48 | deploy: 49 | name: Deploy 50 | environment: 51 | name: github-pages 52 | url: ${{ steps.deployment.outputs.page_url }} 53 | needs: build 54 | runs-on: ubuntu-latest 55 | steps: 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v4 59 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { FetchResponse, MaybeOptionalInit } from "openapi-fetch"; 2 | import type { HttpMethod, MediaType, PathsWithMethod, RequiredKeysOf } from "openapi-typescript-helpers"; 3 | import type { SWRConfiguration, SWRResponse } from "swr"; 4 | 5 | type MaybeRequired = RequiredKeysOf extends never ? T | undefined : T; 6 | 7 | type TryKey = T extends { [Key in K]?: unknown } ? T[K] : undefined; 8 | 9 | /** 10 | * Provides specific types used within a given request 11 | */ 12 | export type TypesForRequest< 13 | Paths extends Record, 14 | Method extends Extract, 15 | Path extends PathsWithMethod, 16 | // --- 17 | Init = MaybeOptionalInit, 18 | Params = Init extends { params?: unknown } ? Init["params"] : undefined, 19 | Res = FetchResponse, 20 | Data = Extract["data"], 21 | Error = Extract["error"], 22 | PathParams = TryKey, 23 | Query = TryKey, 24 | Headers = TryKey, 25 | Cookies = TryKey, 26 | SWRConfig = SWRConfiguration, 27 | > = { 28 | Init: Init; 29 | Data: Data; 30 | Error: Error; 31 | Path: MaybeRequired; 32 | Query: MaybeRequired; 33 | Headers: MaybeRequired; 34 | Cookies: Cookies; 35 | SWRConfig: SWRConfig; 36 | SWRResponse: SWRResponse; 37 | }; 38 | 39 | /** 40 | * Provides specific types for GET requests 41 | * 42 | * Uses {@link TypesForRequest} 43 | */ 44 | export type TypesForGetRequest< 45 | Paths extends Record, 46 | Path extends PathsWithMethod>, 47 | > = TypesForRequest, Path>; 48 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | 3 | // https://vitepress.dev/reference/site-config 4 | export default defineConfig({ 5 | title: "SWR OpenAPI", 6 | description: "Bindings for SWR and OpenAPI schemas", 7 | cleanUrls: true, 8 | base: "/swr-openapi/", 9 | themeConfig: { 10 | outline: "deep", 11 | // https://vitepress.dev/reference/default-theme-config 12 | nav: [ 13 | { text: 'Home', link: '/', }, 14 | { text: 'Getting Started', link: '/getting-started' }, 15 | { text: 'API Reference', link: '/api/hook-builders' } 16 | ], 17 | sidebar: [ 18 | { 19 | text: "Getting Started", 20 | items: [ 21 | {text: "Introduction", link: "/getting-started"}, 22 | {text: "Setup", link: "/getting-started#setup"}, 23 | {text: "Basic Usage", link: "/getting-started#basic-usage"} 24 | ], 25 | }, 26 | { 27 | text: "API Reference", 28 | base: '/api', 29 | items: [ 30 | { 31 | text: 'Hook Builders', 32 | link: '/hook-builders' 33 | }, 34 | { 35 | text: 'useImmutable', 36 | link: '/use-immutable' 37 | }, 38 | { 39 | text: 'useInfinite', 40 | link: '/use-infinite' 41 | }, 42 | { 43 | text: 'useMutate', 44 | link: '/use-mutate' 45 | }, 46 | { 47 | text: 'useQuery', 48 | link: '/use-query' 49 | }, 50 | 51 | ] 52 | }, 53 | ], 54 | socialLinks: [ 55 | { icon: 'github', link: 'https://github.com/htunnicliff/swr-openapi' } 56 | ], 57 | footer: { 58 | message: 'Released under the MIT License' 59 | }, 60 | search: { 61 | provider: "local", 62 | } 63 | } 64 | }) 65 | -------------------------------------------------------------------------------- /src/query-base.ts: -------------------------------------------------------------------------------- 1 | import type { Client } from "openapi-fetch"; 2 | import type { MediaType, PathsWithMethod, RequiredKeysOf } from "openapi-typescript-helpers"; 3 | import { useCallback, useDebugValue, useMemo } from "react"; 4 | import type { Fetcher, SWRHook } from "swr"; 5 | import type { Exact } from "type-fest"; 6 | import type { TypesForGetRequest } from "./types.js"; 7 | 8 | /** 9 | * @private 10 | */ 11 | export function configureBaseQueryHook(useHook: SWRHook) { 12 | return function createQueryBaseHook< 13 | Paths extends {}, 14 | IMediaType extends MediaType, 15 | Prefix extends string, 16 | FetcherError = never, 17 | >(client: Client, prefix: Prefix) { 18 | return function useQuery< 19 | Path extends PathsWithMethod, 20 | R extends TypesForGetRequest, 21 | Init extends Exact, 22 | Data extends R["Data"], 23 | Error extends R["Error"] | FetcherError, 24 | Config extends R["SWRConfig"], 25 | >( 26 | path: Path, 27 | ...[init, config]: RequiredKeysOf extends never ? [(Init | null)?, Config?] : [Init | null, Config?] 28 | ) { 29 | useDebugValue(`${prefix} - ${path as string}`); 30 | 31 | const key = useMemo(() => (init !== null ? ([prefix, path, init] as const) : null), [prefix, path, init]); 32 | 33 | type Key = typeof key; 34 | 35 | // TODO: Lift up fetcher to and remove useCallback 36 | const fetcher: Fetcher = useCallback( 37 | async ([_, path, init]) => { 38 | // @ts-expect-error TODO: Improve internal init types 39 | const res = await client.GET(path, init); 40 | if (res.error) { 41 | throw res.error; 42 | } 43 | return res.data as Data; 44 | }, 45 | [client], 46 | ); 47 | 48 | // @ts-expect-error TODO: Improve internal config types 49 | return useHook(key, fetcher, config); 50 | }; 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

swr-openapi

3 |

4 | 5 |

Generate swr hooks using OpenAPI schemas

6 | 7 |

8 | 9 | npm 10 | 11 | 12 | license 13 | 14 |

15 | 16 | ## Introduction 17 | 18 | swr-openapi is a type-safe wrapper around [`swr`](https://swr.vercel.app). 19 | 20 | It works by using [openapi-fetch](https://openapi-ts.dev/openapi-fetch) and [openapi-typescript](https://openapi-ts.dev/introduction) so you get all the following features: 21 | 22 | - ✅ No typos in URLs or params. 23 | - ✅ All parameters, request bodies, and responses are type-checked and 100% match your schema 24 | - ✅ No manual typing of your API 25 | - ✅ Eliminates `any` types that hide bugs 26 | - ✅ Also eliminates `as` type overrides that can also hide bugs 27 | 28 | ```tsx [src/my-component.ts] 29 | import createClient from "openapi-fetch"; 30 | import { createQueryHook } from "swr-openapi"; 31 | import type { paths } from "./my-openapi-3-schema"; // generated by openapi-typescript 32 | 33 | const client = createClient({ 34 | baseUrl: "https://myapi.dev/v1/", 35 | }); 36 | 37 | const useQuery = createQueryHook(client, "my-api"); 38 | 39 | function MyComponent() { 40 | const { data, error, isLoading, isValidating, mutate } = useQuery( 41 | "/blogposts/{post_id}", 42 | { 43 | params: { 44 | path: { post_id: "123" }, 45 | }, 46 | }, 47 | ); 48 | 49 | if (isLoading || !data) return "Loading..."; 50 | 51 | if (error) return `An error occured: ${error.message}`; 52 | 53 | return
{data.title}
; 54 | } 55 | 56 | ``` 57 | 58 | ## 📓 Docs 59 | 60 | [View Docs](https://htunnicliff.github.io/swr-openapi) 61 | -------------------------------------------------------------------------------- /docs/api/use-query.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useQuery 3 | --- 4 | 5 | # {{ $frontmatter.title }} 6 | 7 | This hook is a typed wrapper over [`useSWR`][swr-api]. 8 | 9 | ```ts 10 | import createClient from "openapi-fetch"; 11 | import { createQueryHook } from "swr-openapi"; 12 | import type { paths } from "./my-schema"; 13 | 14 | const client = createClient(/* ... */); 15 | 16 | const useQuery = createQueryHook(client, "my-api"); 17 | 18 | const { data, error, isLoading, isValidating, mutate } = useQuery( 19 | path, 20 | init, 21 | config, 22 | ); 23 | ``` 24 | 25 | ## API 26 | 27 | ### Parameters 28 | 29 | - `path`: Any endpoint that supports `GET` requests. 30 | - `init`: (_sometimes optional_) 31 | - [Fetch options][oai-fetch-options] for the chosen endpoint. 32 | - `null` to skip the request (see [SWR Conditional Fetching][swr-conditional-fetching]). 33 | - `config`: (_optional_) [SWR options][swr-options]. 34 | 35 | 36 | 37 | ### Returns 38 | 39 | - An [SWR response][swr-response]. 40 | 41 | ## How It Works 42 | 43 | `useQuery` is a very thin wrapper over [`useSWR`][swr-api]. Most of the code involves TypeScript generics that are transpiled away. 44 | 45 | The prefix supplied in `createQueryHook` is joined with `path` and `init` to form the key passed to SWR. 46 | 47 | > `prefix` is only used to help ensure uniqueness for SWR's cache, in the case that two or more API clients share an identical path (e.g. `/api/health`). It is not included in actual `GET` requests. 48 | 49 | Then, `GET` is invoked with `path` and `init`. Short and sweet. 50 | 51 | ```ts 52 | function useQuery(path, ...[init, config]) { 53 | return useSWR( 54 | init !== null ? [prefix, path, init] : null, 55 | async ([_prefix, path, init]) => { 56 | const res = await client.GET(path, init); 57 | if (res.error) { 58 | throw res.error; 59 | } 60 | return res.data; 61 | }, 62 | config, 63 | ); 64 | } 65 | ``` 66 | 67 | 68 | [oai-fetch-options]: https://openapi-ts.pages.dev/openapi-fetch/api#fetch-options 69 | [swr-options]: https://swr.vercel.app/docs/api#options 70 | [swr-conditional-fetching]: https://swr.vercel.app/docs/conditional-fetching#conditional 71 | [swr-response]: https://swr.vercel.app/docs/api#return-values 72 | [swr-api]: https://swr.vercel.app/docs/api 73 | -------------------------------------------------------------------------------- /docs/api/use-mutate.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useMutate 3 | --- 4 | 5 | # {{ $frontmatter.title }} 6 | 7 | `useMutate` is a wrapper around SWR's [global mutate][swr-global-mutate] function. It provides a type-safe mechanism for updating and revalidating SWR's client-side cache for specific endpoints. 8 | 9 | Like global mutate, this mutate wrapper accepts three parameters: `key`, `data`, and `options`. The latter two parameters are identical to those in _bound mutate_. `key` can be either a path alone, or a path with fetch options. 10 | 11 | The level of specificity used when defining the key will determine which cached requests are updated. If only a path is provided, any cached request using that path will be updated. If fetch options are included in the key, the [`compare`](./hook-builders.md#compare) function will determine if a cached request's fetch options match the key's fetch options. 12 | 13 | ```ts 14 | const mutate = useMutate(); 15 | 16 | await mutate([path, init], data, options); 17 | ``` 18 | 19 | ## API 20 | 21 | ### Parameters 22 | 23 | - `key`: 24 | - `path`: Any endpoint that supports `GET` requests. 25 | - `init`: (_optional_) Partial fetch options for the chosen endpoint. 26 | - `data`: (_optional_) 27 | - Data to update the client cache. 28 | - An async function for a remote mutation. 29 | - `options`: (_optional_) [SWR mutate options][swr-mutate-params]. 30 | 31 | ### Returns 32 | 33 | - A promise containing an array, where each array item is either updated data for a matched key or `undefined`. 34 | 35 | > SWR's `mutate` signature specifies that when a matcher function is used, the return type will be [an array](https://github.com/vercel/swr/blob/1585a3e37d90ad0df8097b099db38f1afb43c95d/src/_internal/types.ts#L426). Since our wrapper uses a key matcher function, it will always return an array type. 36 | 37 | 38 | ## How It Works 39 | 40 | ```ts 41 | function useMutate() { 42 | const { mutate } = useSWRConfig(); 43 | return useCallback( 44 | ([path, init], data, opts) => { 45 | return mutate( 46 | (key) => { 47 | if (!Array.isArray(key) || ![2, 3].includes(key.length)) { 48 | return false; 49 | } 50 | const [keyPrefix, keyPath, keyOptions] = key; 51 | return ( 52 | keyPrefix === prefix && 53 | keyPath === path && 54 | (init ? compare(keyOptions, init) : true) 55 | ); 56 | }, 57 | data, 58 | opts, 59 | ); 60 | }, 61 | [mutate, prefix, compare], 62 | ); 63 | } 64 | ``` 65 | 66 | [swr-global-mutate]: https://swr.vercel.app/docs/mutation#global-mutate 67 | [swr-mutate-params]: https://swr.vercel.app/docs/mutation#parameters 68 | -------------------------------------------------------------------------------- /src/infinite.ts: -------------------------------------------------------------------------------- 1 | import type { Client } from "openapi-fetch"; 2 | import type { MediaType, PathsWithMethod } from "openapi-typescript-helpers"; 3 | import { useCallback, useDebugValue } from "react"; 4 | import useSWRInfinite, { 5 | type SWRInfiniteConfiguration, 6 | type SWRInfiniteFetcher, 7 | type SWRInfiniteKeyLoader, 8 | } from "swr/infinite"; 9 | import type { Exact } from "type-fest"; 10 | import type { TypesForGetRequest } from "./types.js"; 11 | 12 | /** 13 | * Produces a typed wrapper for [`useSWRInfinite`](https://swr.vercel.app/docs/pagination#useswrinfinite). 14 | * 15 | * ```ts 16 | * import createClient from "openapi-fetch"; 17 | * const client = createClient(); 18 | * 19 | * const useInfinite = createInfiniteHook(client, ""); 20 | * 21 | * useInfinite("/pets", (index, previousPage) => { 22 | * if (previousPage && !previousPage.hasMore) { 23 | * return null; 24 | * } 25 | * 26 | * return { 27 | * params: { 28 | * query: { 29 | * limit: 10, 30 | * offset: index * 10, 31 | * }, 32 | * }, 33 | * }; 34 | * }); 35 | * ``` 36 | */ 37 | export function createInfiniteHook< 38 | Paths extends {}, 39 | IMediaType extends MediaType, 40 | Prefix extends string, 41 | FetcherError = never, 42 | >(client: Client, prefix: Prefix) { 43 | return function useInfinite< 44 | Path extends PathsWithMethod, 45 | R extends TypesForGetRequest, 46 | Init extends Exact, 47 | Data extends R["Data"], 48 | Error extends R["Error"] | FetcherError, 49 | Config extends SWRInfiniteConfiguration, 50 | >(path: Path, getInit: SWRInfiniteKeyLoader, config?: Config) { 51 | type Key = [Prefix, Path, Init | undefined] | null; 52 | type KeyLoader = SWRInfiniteKeyLoader; 53 | 54 | useDebugValue(`${prefix} - ${path as string}`); 55 | 56 | const fetcher: SWRInfiniteFetcher = useCallback( 57 | async ([_, path, init]) => { 58 | // @ts-expect-error TODO: Improve internal init types 59 | const res = await client.GET(path, init); 60 | if (res.error) { 61 | throw res.error; 62 | } 63 | return res.data as Data; 64 | }, 65 | [client], 66 | ); 67 | 68 | const getKey: KeyLoader = (index, previousPageData) => { 69 | const init = getInit(index, previousPageData); 70 | if (init === null) { 71 | return null; 72 | } 73 | const key: Key = [prefix, path, init]; 74 | return key; 75 | }; 76 | 77 | return useSWRInfinite(getKey, fetcher, config); 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swr-openapi", 3 | "description": "Generate SWR hooks from OpenAPI schemas", 4 | "version": "5.5.0", 5 | "author": { 6 | "name": "Hunter Tunnicliff", 7 | "email": "hunter@tunnicliff.co" 8 | }, 9 | "license": "MIT", 10 | "type": "module", 11 | "main": "./dist/index.mjs", 12 | "exports": { 13 | ".": { 14 | "import": "./dist/index.mjs", 15 | "require": "./dist/index.cjs", 16 | "default": "./dist/index.mjs" 17 | }, 18 | "./package.json": "./package.json" 19 | }, 20 | "sideEffects": false, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/htunnicliff/swr-openapi.git" 24 | }, 25 | "keywords": [ 26 | "swr", 27 | "openapi", 28 | "rest", 29 | "generator", 30 | "client", 31 | "fetch" 32 | ], 33 | "funding": { 34 | "type": "buymeacoffee", 35 | "url": "https://buymeacoffee.com/htunnicliff" 36 | }, 37 | "bugs": { 38 | "url": "https://github.com/htunnicliff/swr-openapi/issues" 39 | }, 40 | "files": [ 41 | "dist", 42 | "src", 43 | "!src/__test__", 44 | "LICENSE", 45 | "README.md" 46 | ], 47 | "scripts": { 48 | "build": "unbuild", 49 | "dev": "vitest --typecheck", 50 | "format": "biome format . --write", 51 | "lint": "pnpm run lint:js && pnpm run lint:ts", 52 | "lint:js": "biome check .", 53 | "lint:ts": "tsc --noEmit", 54 | "prepack": "pnpm run build", 55 | "test": "pnpm run test:js && pnpm run test:exports", 56 | "test:js": "vitest run --typecheck", 57 | "test:exports": "pnpm run build && attw --pack .", 58 | "prepublish": "pnpm run build", 59 | "version": "pnpm run build", 60 | "docs:dev": "vitepress dev docs", 61 | "docs:build": "vitepress build docs", 62 | "docs:preview": "vitepress preview docs" 63 | }, 64 | "peerDependencies": { 65 | "openapi-fetch": ">=0.15", 66 | "openapi-typescript": "7", 67 | "react": ">=18", 68 | "swr": "2", 69 | "typescript": "5" 70 | }, 71 | "peerDependenciesMeta": { 72 | "openapi-typescript": { 73 | "optional": true 74 | } 75 | }, 76 | "dependencies": { 77 | "openapi-typescript-helpers": "^0.0.15", 78 | "type-fest": "^5.0.0" 79 | }, 80 | "devDependencies": { 81 | "@arethetypeswrong/cli": "0.18.2", 82 | "@types/lodash": "4.17.20", 83 | "@types/react": "18.3.24", 84 | "biome": "0.3.3", 85 | "lodash": "4.17.21", 86 | "openapi-fetch": "0.15.0", 87 | "openapi-typescript": "7.10.1", 88 | "react": "18.3.1", 89 | "swr": "2.3.6", 90 | "typescript": "5.9.3", 91 | "unbuild": "3.6.1", 92 | "vitepress": "^1.6.4", 93 | "vitest": "3.2.4" 94 | }, 95 | "packageManager": "pnpm@10.25.0+sha512.5e82639027af37cf832061bcc6d639c219634488e0f2baebe785028a793de7b525ffcd3f7ff574f5e9860654e098fe852ba8ac5dd5cefe1767d23a020a92f501" 96 | } 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional stylelint cache 57 | .stylelintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variable files 69 | .env 70 | .env.* 71 | !.env.example 72 | 73 | # parcel-bundler cache (https://parceljs.org/) 74 | .cache 75 | .parcel-cache 76 | 77 | # Next.js build output 78 | .next 79 | out 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | .output 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and not Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # vuepress v2.x temp and cache directory 96 | .temp 97 | .cache 98 | 99 | # Sveltekit cache directory 100 | .svelte-kit/ 101 | 102 | # vitepress build output 103 | **/.vitepress/dist 104 | 105 | # vitepress cache directory 106 | **/.vitepress/cache 107 | 108 | # Docusaurus cache and generated files 109 | .docusaurus 110 | 111 | # Serverless directories 112 | .serverless/ 113 | 114 | # FuseBox cache 115 | .fusebox/ 116 | 117 | # DynamoDB Local files 118 | .dynamodb/ 119 | 120 | # Firebase cache directory 121 | .firebase/ 122 | 123 | # TernJS port file 124 | .tern-port 125 | 126 | # Stores VSCode versions used for testing VSCode extensions 127 | .vscode-test 128 | 129 | # yarn v3 130 | .pnp.* 131 | .yarn/* 132 | !.yarn/patches 133 | !.yarn/plugins 134 | !.yarn/releases 135 | !.yarn/sdks 136 | !.yarn/versions 137 | 138 | # Vite files 139 | vite.config.js.timestamp-* 140 | vite.config.ts.timestamp-* 141 | .vite/ 142 | -------------------------------------------------------------------------------- /src/mutate.ts: -------------------------------------------------------------------------------- 1 | import type { Client } from "openapi-fetch"; 2 | import type { MediaType, PathsWithMethod } from "openapi-typescript-helpers"; 3 | import { useCallback, useDebugValue } from "react"; 4 | import { type MutatorCallback, type MutatorOptions, useSWRConfig } from "swr"; 5 | import type { Exact, PartialDeep } from "type-fest"; 6 | import type { TypesForGetRequest } from "./types.js"; 7 | 8 | // Types are loose here to support ecosystem utilities like `_.isMatch` 9 | export type CompareFn = (init: any, partialInit: any) => boolean; 10 | 11 | /** 12 | * Produces a typed wrapper for [`useSWRConfig#mutate`](https://swr.vercel.app/docs/mutation). 13 | * 14 | * ```ts 15 | * import createClient from "openapi-fetch"; 16 | * import { isMatch } from "lodash"; 17 | * 18 | * const client = createClient(); 19 | * 20 | * const useMutate = createMutateHook(client, "", isMatch); 21 | * 22 | * const mutate = useMutate(); 23 | * 24 | * // Revalidate all keys matching this path 25 | * await mutate(["/pets"]); 26 | * await mutate(["/pets"], newData); 27 | * await mutate(["/pets"], undefined, { revalidate: true }); 28 | * 29 | * // Revlidate all keys matching this path and this subset of options 30 | * await mutate( 31 | * ["/pets", { query: { limit: 10 } }], 32 | * newData, 33 | * { revalidate: false } 34 | * ); 35 | * ``` 36 | */ 37 | export function createMutateHook( 38 | client: Client, 39 | prefix: string, 40 | compare: CompareFn, 41 | ) { 42 | return function useMutate() { 43 | const { mutate: swrMutate } = useSWRConfig(); 44 | 45 | useDebugValue(prefix); 46 | 47 | return useCallback( 48 | function mutate< 49 | Path extends PathsWithMethod, 50 | R extends TypesForGetRequest, 51 | Init extends Exact, 52 | >( 53 | [path, init]: [Path, PartialDeep?], 54 | data?: R["Data"] | Promise | MutatorCallback, 55 | opts?: boolean | MutatorOptions, 56 | ) { 57 | return swrMutate( 58 | (key) => { 59 | if ( 60 | // Must be array 61 | !Array.isArray(key) || 62 | // Must have 2 or 3 elements (prefix, path, optional init) 63 | ![2, 3].includes(key.length) 64 | ) { 65 | return false; 66 | } 67 | 68 | const [keyPrefix, keyPath, keyOptions] = key as unknown[]; 69 | 70 | return ( 71 | // Matching prefix 72 | keyPrefix === prefix && 73 | // Matching path 74 | keyPath === path && 75 | // Matching options 76 | (init ? compare(keyOptions, init) : true) 77 | ); 78 | }, 79 | data, 80 | opts, 81 | ); 82 | }, 83 | [swrMutate, prefix, compare], 84 | ); 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /docs/api/hook-builders.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hook Builders 3 | --- 4 | 5 | # {{ $frontmatter.title }} 6 | 7 | Hook builders initialize `useQuery`, `useImmutate`, `useInfinite`, and `useMutate`. 8 | 9 | Each builder function accepts an instance of a [fetch client](https://openapi-ts.dev/openapi-fetch/api) and a prefix unique to that client. 10 | 11 | 12 | ::: tip 13 | 14 | Prefixes ensure that `swr` will avoid caching requests from different APIs when requests happen to match (e.g. `GET /health` for "API A" and `GET /health` for "API B"). 15 | 16 | ::: 17 | 18 | ```ts 19 | import createClient from "openapi-fetch"; 20 | import { isMatch } from "lodash-es"; 21 | 22 | import { 23 | createQueryHook, 24 | createImmutableHook, 25 | createInfiniteHook, 26 | createMutateHook, 27 | } from "swr-openapi"; 28 | 29 | import type { paths } from "./my-openapi-3-schema"; // generated by openapi-typescript 30 | 31 | const client = createClient(/* ... */); 32 | const prefix = "my-api"; 33 | 34 | export const useQuery = createQueryHook(client, prefix); 35 | export const useImmutable = createImmutableHook(client, prefix); 36 | export const useInfinite = createInfiniteHook(client, prefix); 37 | export const useMutate = createMutateHook( 38 | client, 39 | prefix, 40 | isMatch, // Or any comparision function 41 | ); 42 | ``` 43 | 44 | ## API 45 | 46 | ### Parameters 47 | 48 | Each builder hook accepts the same initial parameters: 49 | 50 | - `client`: A [fetch client](https://openapi-ts.dev/openapi-fetch/api). 51 | - `prefix`: A prefix unique to the fetch client. 52 | 53 | `createMutateHook` also accepts a third parameter: 54 | 55 | - [`compare`](#compare): A function to compare fetch options). 56 | 57 | ### Returns 58 | 59 | - `createQueryHook` → [`useQuery`](./use-query.md) 60 | - `createImmutableHook` → [`useImmutable`](./use-immutable.md) 61 | - `createInfiniteHook` → [`useInfinite`](./use-infinite.md) 62 | - `createMutateHook` → [`useMutate`](./use-mutate.md) 63 | 64 | ## `compare` 65 | 66 | When calling `createMutateHook`, a function must be provided with the following contract: 67 | 68 | ```ts 69 | type Compare = (init: any, partialInit: object) => boolean; 70 | ``` 71 | 72 | This function is used to determine whether or not a cached request should be updated when `mutate` is called with fetch options. 73 | 74 | My personal recommendation is to use lodash's [`isMatch`][lodash-is-match]: 75 | 76 | > Performs a partial deep comparison between object and source to determine if object contains equivalent property values. 77 | 78 | ```ts 79 | const useMutate = createMutateHook(client, "", isMatch); 80 | 81 | const mutate = useMutate(); 82 | 83 | await mutate([ 84 | "/path", 85 | { 86 | params: { 87 | query: { 88 | version: "beta", 89 | }, 90 | }, 91 | }, 92 | ]); 93 | 94 | // ✅ Would be updated 95 | useQuery("/path", { 96 | params: { 97 | query: { 98 | version: "beta", 99 | }, 100 | }, 101 | }); 102 | 103 | // ✅ Would be updated 104 | useQuery("/path", { 105 | params: { 106 | query: { 107 | version: "beta", 108 | other: true, 109 | example: [1, 2, 3], 110 | }, 111 | }, 112 | }); 113 | 114 | // ❌ Would not be updated 115 | useQuery("/path", { 116 | params: { 117 | query: {}, 118 | }, 119 | }); 120 | 121 | // ❌ Would not be updated 122 | useQuery("/path"); 123 | 124 | // ❌ Would not be updated 125 | useQuery("/path", { 126 | params: { 127 | query: { 128 | version: "alpha", 129 | }, 130 | }, 131 | }); 132 | 133 | // ❌ Would not be updated 134 | useQuery("/path", { 135 | params: { 136 | query: { 137 | different: "items", 138 | }, 139 | }, 140 | }); 141 | ``` 142 | 143 | [lodash-is-match]: https://lodash.com/docs/4.17.15#isMatch 144 | -------------------------------------------------------------------------------- /src/__test__/infinite.test.ts: -------------------------------------------------------------------------------- 1 | import createClient from "openapi-fetch"; 2 | import * as React from "react"; 3 | import * as SWRInfinite from "swr/infinite"; 4 | import { afterEach, describe, expect, it, vi } from "vitest"; 5 | import { createInfiniteHook } from "../infinite.js"; 6 | import type { paths } from "./fixtures/petstore.js"; 7 | 8 | // Mock `useCallback` (return given function as-is) 9 | vi.mock("react"); 10 | const { useCallback, useMemo, useDebugValue } = vi.mocked(React); 11 | useCallback.mockImplementation((fn) => fn); 12 | useMemo.mockImplementation((fn) => fn()); 13 | 14 | // Mock `useSWRInfinite` 15 | vi.mock("swr/infinite"); 16 | const { default: useSWRInfinite } = vi.mocked(SWRInfinite); 17 | 18 | // Mock `client.GET` 19 | const client = createClient(); 20 | const getSpy = vi.spyOn(client, "GET"); 21 | getSpy.mockResolvedValue({ data: undefined, error: undefined }); 22 | 23 | // Create testable useInfinite hook 24 | const useInfinite = createInfiniteHook(client, ""); 25 | 26 | describe("createInfiniteHook", () => { 27 | afterEach(() => { 28 | vi.clearAllMocks(); 29 | }); 30 | 31 | it("passes correct key loader to useSWRInfinite", () => { 32 | const getInit = vi.fn().mockReturnValueOnce({ foo: "bar" }); 33 | 34 | useInfinite("/pet/{petId}", getInit); 35 | 36 | // Invokes `useSWRInfinite` 37 | expect(useSWRInfinite).toHaveBeenCalledTimes(1); 38 | 39 | let getKey = useSWRInfinite.mock.lastCall![0]; 40 | 41 | // Calling `getKey` should invoke `getInit` 42 | const key = getKey(0, null); 43 | 44 | expect(getInit).toHaveBeenCalledTimes(1); 45 | 46 | // `getInit` should be called with key loader arguments 47 | expect(getInit).toHaveBeenCalledWith(0, null); 48 | 49 | // `getKey` should return correct key 50 | expect(key).toMatchObject(["", "/pet/{petId}", { foo: "bar" }]); 51 | 52 | // Key is `null` when `getInit` returns `null` 53 | getInit.mockReturnValueOnce(null); 54 | useInfinite("/pet/{petId}", getInit); 55 | getKey = useSWRInfinite.mock.lastCall![0]; 56 | 57 | expect(getKey(0, null)).toBeNull(); 58 | }); 59 | 60 | it("passes correct fetcher to useSWRInfinite", async () => { 61 | useInfinite("/pet/{petId}", vi.fn()); 62 | 63 | const fetcher = useSWRInfinite.mock.lastCall![1]; 64 | 65 | // client.GET not called until fetcher is called 66 | expect(getSpy).not.toHaveBeenCalled(); 67 | 68 | // Call fetcher 69 | getSpy.mockResolvedValueOnce({ data: "some-data", error: undefined }); 70 | const data = await fetcher!(["some-key", "any-path", { some: "init" }]); 71 | 72 | expect(getSpy).toHaveBeenLastCalledWith("any-path", { some: "init" }); 73 | expect(data).toBe("some-data"); 74 | 75 | // Call fetcher with error 76 | getSpy.mockResolvedValueOnce({ 77 | data: undefined, 78 | error: new Error("Yikes"), 79 | }); 80 | 81 | await expect(() => fetcher!(["some-key", "any-path", { some: "init" }])).rejects.toThrow(new Error("Yikes")); 82 | }); 83 | 84 | it("passes correct config to useSWRInfinite", () => { 85 | useInfinite("/pet/{petId}", vi.fn(), { initialSize: 5000 }); 86 | 87 | expect(useSWRInfinite).toHaveBeenLastCalledWith(expect.any(Function), expect.any(Function), { initialSize: 5000 }); 88 | 89 | useInfinite("/pet/{petId}", vi.fn()); 90 | 91 | expect(useSWRInfinite).toHaveBeenLastCalledWith(expect.any(Function), expect.any(Function), undefined); 92 | }); 93 | 94 | it("invokes debug value hook with path", () => { 95 | useInfinite("/pet/findByStatus", () => null); 96 | 97 | expect(useDebugValue).toHaveBeenLastCalledWith(" - /pet/findByStatus"); 98 | 99 | useInfinite("/pet/findByTags", () => ({ 100 | params: { query: { tags: ["tag1"] } }, 101 | })); 102 | 103 | expect(useDebugValue).toHaveBeenLastCalledWith(" - /pet/findByTags"); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /src/__test__/query-base.test.ts: -------------------------------------------------------------------------------- 1 | import createClient from "openapi-fetch"; 2 | import * as React from "react"; 3 | import * as SWR from "swr"; 4 | import { afterEach, describe, expect, it, vi } from "vitest"; 5 | import { configureBaseQueryHook } from "../query-base.js"; 6 | import type { paths } from "./fixtures/petstore.js"; 7 | 8 | // Mock `useCallback` (return given function as-is) 9 | vi.mock("react"); 10 | const { useCallback, useMemo, useDebugValue } = vi.mocked(React); 11 | useCallback.mockImplementation((fn) => fn); 12 | useMemo.mockImplementation((fn) => fn()); 13 | 14 | // Mock `useSWR` 15 | vi.mock("swr"); 16 | const { default: useSWR } = vi.mocked(SWR); 17 | useSWR.mockReturnValue({ 18 | data: undefined, 19 | error: undefined, 20 | isLoading: false, 21 | isValidating: false, 22 | mutate: vi.fn(), 23 | }); 24 | 25 | // Mock `client.GET` 26 | const client = createClient(); 27 | const getSpy = vi.spyOn(client, "GET"); 28 | getSpy.mockResolvedValue({ data: undefined, error: undefined }); 29 | // Create testable useQuery hook 30 | const createQueryHook = configureBaseQueryHook(useSWR); 31 | const useQuery = createQueryHook(client, ""); 32 | 33 | describe("configureBaseQueryHook", () => { 34 | afterEach(() => { 35 | vi.clearAllMocks(); 36 | }); 37 | 38 | it("passes correct key to useSWR", () => { 39 | useQuery("/store/inventory", {}); 40 | 41 | expect(useSWR).toHaveBeenLastCalledWith(["", "/store/inventory", {}], expect.any(Function), undefined); 42 | 43 | useQuery("/pet/findByTags", { 44 | params: { 45 | query: { 46 | tags: ["tag1", "tag2"], 47 | }, 48 | }, 49 | }); 50 | 51 | expect(useSWR).toHaveBeenLastCalledWith( 52 | [ 53 | "", 54 | "/pet/findByTags", 55 | { 56 | params: { 57 | query: { 58 | tags: ["tag1", "tag2"], 59 | }, 60 | }, 61 | }, 62 | ], 63 | expect.any(Function), 64 | undefined, 65 | ); 66 | 67 | useQuery("/store/inventory"); 68 | 69 | expect(useSWR).toHaveBeenLastCalledWith( 70 | ["", "/store/inventory", undefined], 71 | expect.any(Function), 72 | undefined, 73 | ); 74 | }); 75 | 76 | it("passes correct fetcher to useSWR", async () => { 77 | // Note: useQuery input doesn't matter here, since we test the fetcher in isolation 78 | useQuery("/pet/findByStatus"); 79 | 80 | const fetcher = useSWR.mock.lastCall![1]; 81 | 82 | // client.GET not called until fetcher is called 83 | expect(getSpy).not.toHaveBeenCalled(); 84 | 85 | // Call fetcher 86 | getSpy.mockResolvedValueOnce({ data: "some-data", error: undefined }); 87 | const data = await fetcher!(["some-key", "any-path", { some: "init" }]); 88 | 89 | expect(getSpy).toHaveBeenLastCalledWith("any-path", { some: "init" }); 90 | expect(data).toBe("some-data"); 91 | 92 | // Call fetcher with error 93 | getSpy.mockResolvedValueOnce({ 94 | data: undefined, 95 | error: new Error("Yikes"), 96 | }); 97 | 98 | await expect(() => fetcher!(["some-key", "any-path", { some: "init" }])).rejects.toThrow(new Error("Yikes")); 99 | }); 100 | 101 | it("passes correct config to useSWR", () => { 102 | useQuery("/pet/findByStatus", {}, { errorRetryCount: 56 }); 103 | 104 | expect(useSWR).toHaveBeenLastCalledWith(["", "/pet/findByStatus", {}], expect.any(Function), { 105 | errorRetryCount: 56, 106 | }); 107 | }); 108 | 109 | it("invokes debug value hook with path", () => { 110 | useQuery("/pet/findByStatus"); 111 | 112 | expect(useDebugValue).toHaveBeenLastCalledWith(" - /pet/findByStatus"); 113 | 114 | useQuery("/pet/{petId}", { params: { path: { petId: 4 } } }); 115 | 116 | expect(useDebugValue).toHaveBeenLastCalledWith(" - /pet/{petId}"); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /docs/api/use-infinite.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useInfinite 3 | --- 4 | 5 | # {{ $frontmatter.title }} 6 | 7 | This hook is a typed wrapper over [`useSWRInfinite`][swr-infinite]. 8 | 9 | ```ts 10 | import createClient from "openapi-fetch"; 11 | import { createInfiniteHook } from "swr-openapi"; 12 | import type { paths } from "./my-schema"; 13 | 14 | const client = createClient(/* ... */); 15 | 16 | const useInfinite = createInfiniteHook(client, "my-api"); 17 | 18 | const { 19 | data, 20 | error, 21 | isLoading, 22 | isValidating, 23 | mutate, 24 | size, 25 | setSize 26 | } = useInfinite(path, getInit, config); 27 | ``` 28 | 29 | ## API 30 | 31 | ### Parameters 32 | 33 | - `path`: Any endpoint that supports `GET` requests. 34 | - [`getInit`](#getinit): A function that returns the fetch options for a given page. 35 | - `config`: (_optional_) [SWR infinite options][swr-infinite-options]. 36 | 37 | ### Returns 38 | 39 | - An [SWR infinite response][swr-infinite-return]. 40 | 41 | ## `getInit` 42 | 43 | This function is similar to the [`getKey`][swr-infinite-options] parameter accepted by `useSWRInfinite`, with some slight alterations to take advantage of Open API types. 44 | 45 | ### Parameters 46 | 47 | - `pageIndex`: The zero-based index of the current page to load. 48 | - `previousPageData`: 49 | - `undefined` (if on the first page). 50 | - The fetched response for the last page retrieved. 51 | 52 | ### Returns 53 | 54 | - [Fetch options][oai-fetch-options] for the next page to load. 55 | - `null` if no more pages should be loaded. 56 | 57 | ## Examples 58 | 59 | ### Using limit and offset 60 | 61 | ```ts 62 | useInfinite("/something", (pageIndex, previousPageData) => { 63 | // No more pages 64 | if (previousPageData && !previousPageData.hasMore) { 65 | return null; 66 | } 67 | 68 | // First page 69 | if (!previousPageData) { 70 | return { 71 | params: { 72 | query: { 73 | limit: 10, 74 | }, 75 | }, 76 | }; 77 | } 78 | 79 | // Next page 80 | return { 81 | params: { 82 | query: { 83 | limit: 10, 84 | offset: 10 * pageIndex, 85 | }, 86 | }, 87 | }; 88 | }); 89 | ``` 90 | 91 | ### Using cursors 92 | 93 | ```ts 94 | useInfinite("/something", (pageIndex, previousPageData) => { 95 | // No more pages 96 | if (previousPageData && !previousPageData.nextCursor) { 97 | return null; 98 | } 99 | 100 | // First page 101 | if (!previousPageData) { 102 | return { 103 | params: { 104 | query: { 105 | limit: 10, 106 | }, 107 | }, 108 | }; 109 | } 110 | 111 | // Next page 112 | return { 113 | params: { 114 | query: { 115 | limit: 10, 116 | cursor: previousPageData.nextCursor, 117 | }, 118 | }, 119 | }; 120 | }); 121 | ``` 122 | 123 | ## How It Works 124 | 125 | Just as [`useQuery`](./use-query.md) is a thin wrapper over [`useSWR`][swr-api], `useInfinite` is a thin wrapper over [`useSWRInfinite`][swr-infinite]. 126 | 127 | Instead of using static [fetch options][oai-fetch-options] as part of the SWR key, `useInfinite` is given a function ([`getInit`](#getinit)) that should dynamically determines the fetch options based on the current page index and the data from a previous page. 128 | 129 | ```ts 130 | function useInfinite(path, getInit, config) { 131 | const fetcher = async ([_, path, init]) => { 132 | const res = await client.GET(path, init); 133 | if (res.error) { 134 | throw res.error; 135 | } 136 | return res.data; 137 | }; 138 | const getKey = (index, previousPageData) => { 139 | const init = getInit(index, previousPageData); 140 | if (init === null) { 141 | return null; 142 | } 143 | const key = [prefix, path, init]; 144 | return key; 145 | }; 146 | return useSWRInfinite(getKey, fetcher, config); 147 | } 148 | ``` 149 | 150 | 151 | [oai-fetch-options]: https://openapi-ts.pages.dev/openapi-fetch/api#fetch-options 152 | [swr-api]: https://swr.vercel.app/docs/api 153 | [swr-infinite]: https://swr.vercel.app/docs/pagination#useswrinfinite 154 | [swr-infinite-return]: https://swr.vercel.app/docs/pagination#return-values 155 | [swr-infinite-options]: https://swr.vercel.app/docs/pagination#parameters 156 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | --- 4 | 5 | # {{ $frontmatter.title }} 6 | 7 | ## Introduction 8 | 9 | swr-openapi is a type-safe wrapper around [`swr`](https://swr.vercel.app). 10 | 11 | It works by using [openapi-fetch](https://openapi-ts.dev/openapi-fetch) and [openapi-typescript](https://openapi-ts.dev/introduction) so you get all the following features: 12 | 13 | - ✅ No typos in URLs or params. 14 | - ✅ All parameters, request bodies, and responses are type-checked and 100% match your schema 15 | - ✅ No manual typing of your API 16 | - ✅ Eliminates `any` types that hide bugs 17 | - ✅ Also eliminates `as` type overrides that can also hide bugs 18 | 19 | ::: code-group 20 | 21 | ```tsx [src/my-component.ts] 22 | import createClient from "openapi-fetch"; 23 | import { createQueryHook } from "swr-openapi"; 24 | import type { paths } from "./my-openapi-3-schema"; // generated by openapi-typescript 25 | 26 | const client = createClient({ 27 | baseUrl: "https://myapi.dev/v1/", 28 | }); 29 | 30 | const useQuery = createQueryHook(client, "my-api"); 31 | 32 | function MyComponent() { 33 | const { data, error, isLoading, isValidating, mutate } = useQuery( 34 | "/blogposts/{post_id}", 35 | { 36 | params: { 37 | path: { post_id: "123" }, 38 | }, 39 | }, 40 | ); 41 | 42 | if (isLoading || !data) return "Loading..."; 43 | 44 | if (error) return `An error occured: ${error.message}`; 45 | 46 | return
{data.title}
; 47 | } 48 | 49 | ``` 50 | 51 | ::: 52 | 53 | ## Setup 54 | 55 | Install this library along with [openapi-fetch](https://openapi-ts.dev/openapi-fetch) and [openapi-typescript](https://openapi-ts.dev/introduction): 56 | 57 | ```bash 58 | npm i swr-openapi openapi-fetch 59 | npm i -D openapi-typescript typescript 60 | ``` 61 | 62 | ::: tip Highly recommended 63 | 64 | Enable [noUncheckedIndexedAccess](https://www.typescriptlang.org/tsconfig#noUncheckedIndexedAccess) in your `tsconfig.json` ([docs](https://openapi-ts.dev/advanced#enable-nouncheckedindexedaccess-in-tsconfig)) 65 | 66 | ::: 67 | 68 | Next, generate TypeScript types from your OpenAPI schema using openapi-typescript: 69 | 70 | ```bash 71 | npx openapi-typescript ./path/to/api/v1.yaml -o ./src/lib/api/v1.d.ts 72 | ``` 73 | 74 | ## Basic Usage 75 | 76 | Once types have been generated from your schema, you can create a [fetch client](https://openapi-ts.dev/openapi-fetch/) and export wrapped `swr` hooks. 77 | 78 | 79 | Wrapper hooks are provided 1:1 for each hook exported by SWR. Check out the other sections of this documentation to learn more about each one. 80 | 81 | ::: code-group 82 | 83 | ```ts [src/my-api.ts] 84 | import createClient from "openapi-fetch"; 85 | import { 86 | createQueryHook, 87 | createImmutableHook, 88 | createInfiniteHook, 89 | createMutateHook, 90 | } from "swr-openapi"; 91 | import { isMatch } from "lodash-es"; 92 | import type { paths } from "./my-openapi-3-schema"; // generated by openapi-typescript 93 | 94 | const client = createClient({ 95 | baseUrl: "https://myapi.dev/v1/", 96 | }); 97 | const prefix = "my-api"; 98 | 99 | export const useQuery = createQueryHook(client, prefix); 100 | export const useImmutable = createImmutableHook(client, prefix); 101 | export const useInfinite = createInfiniteHook(client, prefix); 102 | export const useMutate = createMutateHook( 103 | client, 104 | prefix, 105 | isMatch, // Or any comparision function 106 | ); 107 | ``` 108 | ::: 109 | 110 | ::: tip 111 | You can find more information about `createClient` on the [openapi-fetch documentation](https://openapi-ts.dev/openapi-fetch/api#createclient). 112 | ::: 113 | 114 | 115 | Then, import these hooks in your components: 116 | 117 | ::: code-group 118 | ```tsx [src/my-component.tsx] 119 | import { useQuery } from "./my-api"; 120 | 121 | function MyComponent() { 122 | const { data, error, isLoading, isValidating, mutate } = useQuery( 123 | "/blogposts/{post_id}", 124 | { 125 | params: { 126 | path: { post_id: "123" }, 127 | }, 128 | }, 129 | ); 130 | 131 | if (isLoading || !data) return "Loading..."; 132 | 133 | if (error) return `An error occured: ${error.message}`; 134 | 135 | return
{data.title}
; 136 | } 137 | ``` 138 | ::: 139 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # swr-openapi 2 | 3 | ## 5.4.2 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [[`bdd5ddb`](https://github.com/openapi-ts/openapi-typescript/commit/bdd5ddb7d5f8463bd0515f0b2d5c98a8a394dabf), [`0f22be2`](https://github.com/openapi-ts/openapi-typescript/commit/0f22be218f0c8050a96f35a6a271b959b2c5a23f), [`8f96eb5`](https://github.com/openapi-ts/openapi-typescript/commit/8f96eb50f5ec060e2e9100e9a43d3fe98e9795c5)]: 8 | - openapi-typescript@7.10.0 9 | - openapi-fetch@0.15.0 10 | 11 | ## 5.4.1 12 | 13 | ### Patch Changes 14 | 15 | - Updated dependencies [[`7527d1e`](https://github.com/openapi-ts/openapi-typescript/commit/7527d1e7502cd1e9621922f028b4736d85f25800)]: 16 | - openapi-fetch@0.14.1 17 | 18 | ## 5.4.0 19 | 20 | ### Minor Changes 21 | 22 | - [#2420](https://github.com/openapi-ts/openapi-typescript/pull/2420) [`48134b5`](https://github.com/openapi-ts/openapi-typescript/commit/48134b59f1bb9f7c7daac6f9f4a8b8c6422ef9a2) Thanks [@htunnicliff](https://github.com/htunnicliff)! - Disallow extra properties in swr-openapi init types 23 | 24 | ## 5.3.1 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies [[`6943ccf`](https://github.com/openapi-ts/openapi-typescript/commit/6943ccf216f602f004eb178dd652ffcbafc05346), [`5be22d7`](https://github.com/openapi-ts/openapi-typescript/commit/5be22d7adc8bc36fdfa91f1aa3473919107060f2)]: 29 | - openapi-typescript@7.9.1 30 | 31 | ## 5.3.0 32 | 33 | ### Minor Changes 34 | 35 | - [#2310](https://github.com/openapi-ts/openapi-typescript/pull/2310) [`e66b5ce`](https://github.com/openapi-ts/openapi-typescript/commit/e66b5ce63bfcdc57c6ee942e5ed4e7667e64c290) Thanks [@drwpow](https://github.com/drwpow)! - Build package with unbuild to improve CJS support 36 | 37 | ### Patch Changes 38 | 39 | - Updated dependencies [[`e66b5ce`](https://github.com/openapi-ts/openapi-typescript/commit/e66b5ce63bfcdc57c6ee942e5ed4e7667e64c290), [`e66b5ce`](https://github.com/openapi-ts/openapi-typescript/commit/e66b5ce63bfcdc57c6ee942e5ed4e7667e64c290)]: 40 | - openapi-fetch@0.14.0 41 | - openapi-typescript@7.8.0 42 | 43 | ## 5.2.0 44 | 45 | ### Minor Changes 46 | 47 | - [#2147](https://github.com/openapi-ts/openapi-typescript/pull/2147) [`5848759`](https://github.com/openapi-ts/openapi-typescript/commit/5848759e3b6796331b0e85bf26a01c14af90537f) Thanks [@SSlime-s](https://github.com/SSlime-s)! - Add custom error types to query builder 48 | 49 | ## 5.1.7 50 | 51 | ### Patch Changes 52 | 53 | - Updated dependencies [[`81c031d`](https://github.com/openapi-ts/openapi-typescript/commit/81c031da8584ed49b033ebfc67bbb3e1ca258699)]: 54 | - openapi-fetch@0.13.8 55 | 56 | ## 5.1.6 57 | 58 | ### Patch Changes 59 | 60 | - Updated dependencies [[`30c6da8`](https://github.com/openapi-ts/openapi-typescript/commit/30c6da800a00bda87da66dea6d04807e1379f06a), [`7205e12`](https://github.com/openapi-ts/openapi-typescript/commit/7205e12e07e5fd36a6bb3be44ea911f57bbbeb60)]: 61 | - openapi-fetch@0.13.7 62 | - openapi-typescript@7.7.1 63 | 64 | ## 5.1.5 65 | 66 | ### Patch Changes 67 | 68 | - Updated dependencies [[`4966560`](https://github.com/openapi-ts/openapi-typescript/commit/4966560790ad49fabb06d718115a82a779a5b74a), [`fc3f7f8`](https://github.com/openapi-ts/openapi-typescript/commit/fc3f7f8b9cf52f0d4daf31ed4579d588c5b0f3e6), [`7f3f7b6`](https://github.com/openapi-ts/openapi-typescript/commit/7f3f7b65da5ef8caf5304486184118352665eb3f), [`47e4b5e`](https://github.com/openapi-ts/openapi-typescript/commit/47e4b5eb86adc59e3de2a4179741d35a26db61c0), [`ef23947`](https://github.com/openapi-ts/openapi-typescript/commit/ef239479b5f15fc4c98dd15c72974d4cb8722fb0)]: 69 | - openapi-fetch@0.13.6 70 | - openapi-typescript@7.7.0 71 | 72 | ## 5.1.4 73 | 74 | ### Patch Changes 75 | 76 | - Updated dependencies [[`ebe56f3`](https://github.com/openapi-ts/openapi-typescript/commit/ebe56f337561bfdd1bf1abdc56ba3d2f48c4d393)]: 77 | - openapi-fetch@0.13.5 78 | 79 | ## 5.1.3 80 | 81 | ### Patch Changes 82 | 83 | - Updated dependencies [[`2bffe2a`](https://github.com/openapi-ts/openapi-typescript/commit/2bffe2a652864a54c8dc969327e4a8eb4081eb25)]: 84 | - openapi-typescript@7.5.2 85 | - openapi-fetch@0.13.4 86 | 87 | ## 5.1.2 88 | 89 | ### Patch Changes 90 | 91 | - Updated dependencies [[`7081842`](https://github.com/openapi-ts/openapi-typescript/commit/70818420c1cd6ca2ad2529bf2d7936bd01f3ef42)]: 92 | - openapi-fetch@0.13.2 93 | 94 | ## 5.1.1 95 | 96 | ### Patch Changes 97 | 98 | - Updated dependencies [[`35c576c`](https://github.com/openapi-ts/openapi-typescript/commit/35c576c8b2852f66e641014d13ffcfdeb21e98a1), [`e2d8541`](https://github.com/openapi-ts/openapi-typescript/commit/e2d854131a1dc11d3b8e8513d3e0ce1f04ea1211)]: 99 | - openapi-fetch@0.13.1 100 | - openapi-typescript@7.4.4 101 | 102 | ## 5.1.0 103 | 104 | ### Minor Changes 105 | 106 | - [#1932](https://github.com/openapi-ts/openapi-typescript/pull/1932) [`639ec45`](https://github.com/openapi-ts/openapi-typescript/commit/639ec45ed9155d2bc0c3d0fbebd3bc52f90ca7eb) Thanks [@htunnicliff](https://github.com/htunnicliff)! - Modify package.json to point to new repository 107 | 108 | - [#1993](https://github.com/openapi-ts/openapi-typescript/pull/1993) [`d95c474`](https://github.com/openapi-ts/openapi-typescript/commit/d95c474bc3eab790e93029ac802e18b79a311fba) Thanks [@htunnicliff](https://github.com/htunnicliff)! - Update dependencies and adjust paths signature to conform to updated helper signature 109 | -------------------------------------------------------------------------------- /src/__test__/mutate.test.ts: -------------------------------------------------------------------------------- 1 | import { isMatch } from "lodash"; 2 | import createClient from "openapi-fetch"; 3 | import * as React from "react"; 4 | import * as SWR from "swr"; 5 | import type { ScopedMutator } from "swr/_internal"; 6 | import { afterEach, describe, expect, it, vi } from "vitest"; 7 | import { createMutateHook } from "../mutate.js"; 8 | import type { paths } from "./fixtures/petstore.js"; 9 | 10 | // Mock `useCallback` (return given function as-is) 11 | vi.mock("react"); 12 | const { useCallback, useDebugValue } = vi.mocked(React); 13 | useCallback.mockImplementation((fn) => fn); 14 | 15 | // Mock `useSWRConfig` 16 | const swrMutate = vi.fn(); 17 | vi.mock("swr"); 18 | const { useSWRConfig } = vi.mocked(SWR); 19 | // @ts-expect-error - only `mutate` is relevant to this test 20 | useSWRConfig.mockReturnValue({ mutate: swrMutate }); 21 | 22 | // Setup 23 | const client = createClient(); 24 | const getKeyMatcher = () => { 25 | if (swrMutate.mock.calls.length === 0) { 26 | throw new Error("swr `mutate` not called"); 27 | } 28 | return swrMutate.mock.lastCall![0] as ScopedMutator; 29 | }; 30 | 31 | const useMutate = createMutateHook( 32 | client, 33 | "", 34 | // @ts-expect-error - not going to compare for most tests 35 | null, 36 | ); 37 | // biome-ignore lint/correctness/useHookAtTopLevel: this is a test 38 | const mutate = useMutate(); 39 | 40 | describe("createMutateHook", () => { 41 | afterEach(() => { 42 | vi.clearAllMocks(); 43 | }); 44 | 45 | it("returns callback that invokes swr `mutate` with fn, data and options", async () => { 46 | expect(swrMutate).not.toHaveBeenCalled(); 47 | 48 | const data = [{ name: "doggie", photoUrls: ["https://example.com"] }]; 49 | const config = { throwOnError: false }; 50 | 51 | await mutate(["/pet/findByStatus"], data, config); 52 | 53 | expect(swrMutate).toHaveBeenCalledTimes(1); 54 | expect(swrMutate).toHaveBeenLastCalledWith( 55 | // Matcher function 56 | expect.any(Function), 57 | // Data 58 | data, 59 | // Config 60 | config, 61 | ); 62 | }); 63 | 64 | it("supports boolean for options argument", async () => { 65 | expect(swrMutate).not.toHaveBeenCalled(); 66 | 67 | const data = [{ name: "doggie", photoUrls: ["https://example.com"] }]; 68 | 69 | await mutate(["/pet/findByStatus"], data, false); 70 | 71 | expect(swrMutate).toHaveBeenCalledTimes(1); 72 | expect(swrMutate).toHaveBeenLastCalledWith( 73 | // Matcher function 74 | expect.any(Function), 75 | // Data 76 | data, 77 | // Config 78 | false, 79 | ); 80 | }); 81 | 82 | it("invokes debug value hook with client prefix", () => { 83 | useMutate(); 84 | 85 | expect(useDebugValue).toHaveBeenLastCalledWith(""); 86 | }); 87 | 88 | describe("useMutate -> mutate -> key matcher", () => { 89 | it("returns false for non-array keys", async () => { 90 | await mutate(["/pet/findByStatus"]); 91 | const keyMatcher = getKeyMatcher(); 92 | 93 | expect(keyMatcher(null)).toBe(false); 94 | expect(keyMatcher(undefined)).toBe(false); 95 | expect(keyMatcher("")).toBe(false); 96 | expect(keyMatcher({})).toBe(false); 97 | }); 98 | 99 | it("returns false for arrays with length !== 3", async () => { 100 | await mutate(["/pet/findByStatus"]); 101 | const keyMatcher = getKeyMatcher(); 102 | 103 | expect(keyMatcher(Array(0))).toBe(false); 104 | expect(keyMatcher(Array(1))).toBe(false); 105 | expect(keyMatcher(Array(2))).toBe(false); 106 | expect(keyMatcher(Array(4))).toBe(false); 107 | expect(keyMatcher(Array(5))).toBe(false); 108 | }); 109 | 110 | it("matches when prefix and path are equal and init isn't given", async () => { 111 | await mutate(["/pet/findByStatus"]); 112 | const keyMatcher = getKeyMatcher(); 113 | 114 | // Same path, no init 115 | expect(keyMatcher(["", "/pet/findByStatus"])).toBe(true); 116 | 117 | // Same path, init ignored 118 | expect(keyMatcher(["", "/pet/findByStatus", { some: "init" }])).toBe(true); 119 | 120 | // Same path, undefined init ignored 121 | expect(keyMatcher(["", "/pet/findByStatus", undefined])).toBe(true); 122 | }); 123 | 124 | it("returns compare result when prefix and path are equal and init is given", async () => { 125 | const psudeoCompare = vi.fn().mockReturnValue("booleanPlaceholder"); 126 | 127 | const prefix = ""; 128 | const path = "/pet/findByStatus"; 129 | const givenInit = {}; 130 | 131 | const useMutate = createMutateHook(client, prefix, psudeoCompare); 132 | const mutate = useMutate(); 133 | await mutate([path, givenInit]); 134 | const keyMatcher = getKeyMatcher(); 135 | 136 | const result = keyMatcher([ 137 | prefix, // Same prefix -> true 138 | path, // Same path -> true 139 | { some: "init" }, // Init -> should call `compare` 140 | ]); 141 | 142 | expect(psudeoCompare).toHaveBeenLastCalledWith( 143 | { some: "init" }, // Init from key 144 | givenInit, // Init given to compare 145 | ); 146 | 147 | // Note: compare result is returned (real world would be boolean) 148 | expect(result).toBe("booleanPlaceholder"); 149 | }); 150 | }); 151 | }); 152 | 153 | describe("createMutateHook with lodash.isMatch as `compare`", () => { 154 | const useMutate = createMutateHook(client, "", isMatch); 155 | const mutate = useMutate(); 156 | 157 | it("returns true when init is a subset of key init", async () => { 158 | await mutate([ 159 | "/pet/findByTags", 160 | { 161 | params: { 162 | query: { 163 | tags: ["tag1"], 164 | }, 165 | }, 166 | }, 167 | ]); 168 | const keyMatcher = getKeyMatcher(); 169 | 170 | expect( 171 | keyMatcher([ 172 | "", 173 | "/pet/findByTags", 174 | { 175 | params: { 176 | query: { 177 | other: "value", 178 | tags: ["tag1", "tag2"], 179 | }, 180 | }, 181 | }, 182 | ]), 183 | ).toBe(true); 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /src/__test__/types.test-d.ts: -------------------------------------------------------------------------------- 1 | import createClient from "openapi-fetch"; 2 | import type { ErrorResponseJSON } from "openapi-typescript-helpers"; 3 | import * as SWR from "swr"; 4 | import type { ScopedMutator } from "swr/_internal"; 5 | import { describe, expectTypeOf, it, vi } from "vitest"; 6 | import { createImmutableHook } from "../immutable.js"; 7 | import { createInfiniteHook } from "../infinite.js"; 8 | import { createMutateHook } from "../mutate.js"; 9 | import { createQueryHook } from "../query.js"; 10 | import type { TypesForRequest } from "../types.js"; 11 | import type { components, paths } from "./fixtures/petstore.js"; 12 | 13 | // Mock `useSWRConfig` 14 | const swrMutate = vi.fn(); 15 | vi.mock("swr"); 16 | const { useSWRConfig } = vi.mocked(SWR); 17 | // @ts-expect-error - only `mutate` is relevant to this test 18 | useSWRConfig.mockReturnValue({ mutate: swrMutate }); 19 | 20 | // Types 21 | type Pet = components["schemas"]["Pet"]; 22 | type PetInvalid = ErrorResponseJSON; 23 | type PetStatusInvalid = ErrorResponseJSON; 24 | expectTypeOf().toMatchTypeOf<{ name: string }>(); 25 | expectTypeOf().toMatchTypeOf<{ message: string } | undefined>(); 26 | expectTypeOf().toMatchTypeOf<{ message: string }>(); 27 | 28 | // Set up hooks 29 | const client = createClient(); 30 | const useQuery = createQueryHook(client, ""); 31 | const useImmutable = createImmutableHook(client, ""); 32 | const useInfinite = createInfiniteHook(client, ""); 33 | const useMutate = createMutateHook( 34 | client, 35 | "", 36 | // @ts-expect-error - compare function not needed for these type tests 37 | null, 38 | ); 39 | // biome-ignore lint/correctness/useHookAtTopLevel: this is a test 40 | const mutate = useMutate(); 41 | 42 | describe("types", () => { 43 | describe("key types", () => { 44 | describe("useQuery", () => { 45 | it("accepts config", () => { 46 | useQuery("/pet/findByStatus", null, { errorRetryCount: 1 }); 47 | }); 48 | 49 | describe("when init is required", () => { 50 | it("does not accept path alone", () => { 51 | // @ts-expect-error path is required 52 | useQuery("/pet/{petId}"); 53 | }); 54 | 55 | it("accepts path and init", () => { 56 | useQuery("/pet/{petId}", { 57 | params: { 58 | path: { 59 | petId: 5, 60 | }, 61 | }, 62 | }); 63 | }); 64 | 65 | it("accepts `null` init", () => { 66 | useQuery("/pet/{petId}", null); 67 | }); 68 | }); 69 | 70 | describe("when init is not required", () => { 71 | it("accepts path alone", () => { 72 | useQuery("/pet/findByStatus"); 73 | }); 74 | 75 | it("accepts path and init", () => { 76 | useQuery("/pet/findByStatus", { 77 | params: { 78 | query: { 79 | status: "available", 80 | }, 81 | }, 82 | }); 83 | }); 84 | 85 | it("accepts `null` init", () => { 86 | useQuery("/pet/findByStatus", null); 87 | }); 88 | }); 89 | 90 | describe("rejects extra properties", () => { 91 | it("in query params", () => { 92 | useQuery("/pet/findByStatus", { 93 | params: { 94 | query: { 95 | status: "available", 96 | // @ts-expect-error extra property should be rejected 97 | invalid_property: "nope", 98 | }, 99 | }, 100 | }); 101 | }); 102 | 103 | it("in path params", () => { 104 | useQuery("/pet/{petId}", { 105 | params: { 106 | path: { 107 | petId: 5, 108 | // @ts-expect-error extra property should be rejected 109 | invalid_path_param: "nope", 110 | }, 111 | }, 112 | }); 113 | }); 114 | 115 | it("in header params", () => { 116 | useQuery("/pet/findByStatus", { 117 | params: { 118 | header: { 119 | "X-Example": "test", 120 | // @ts-expect-error extra property should be rejected 121 | "Invalid-Header": "nope", 122 | }, 123 | }, 124 | }); 125 | }); 126 | }); 127 | }); 128 | 129 | describe("useImmutable", () => { 130 | it("accepts config", () => { 131 | useImmutable("/pet/findByStatus", null, { errorRetryCount: 1 }); 132 | }); 133 | 134 | describe("when init is required", () => { 135 | it("does not accept path alone", () => { 136 | // @ts-expect-error path is required 137 | useImmutable("/pet/{petId}"); 138 | }); 139 | 140 | it("accepts path and init when init is required", () => { 141 | useImmutable("/pet/{petId}", { 142 | params: { 143 | path: { 144 | petId: 5, 145 | }, 146 | }, 147 | }); 148 | }); 149 | 150 | it("accepts `null` init", () => { 151 | useImmutable("/pet/{petId}", null); 152 | }); 153 | }); 154 | 155 | describe("when init is not required", () => { 156 | it("accepts path alone", () => { 157 | useImmutable("/pet/findByStatus"); 158 | }); 159 | 160 | it("accepts `null` init", () => { 161 | useImmutable("/pet/findByStatus", null); 162 | }); 163 | }); 164 | 165 | describe("rejects extra properties", () => { 166 | it("in query params", () => { 167 | useImmutable("/pet/findByStatus", { 168 | params: { 169 | query: { 170 | status: "available", 171 | // @ts-expect-error extra property should be rejected 172 | invalid_property: "nope", 173 | }, 174 | }, 175 | }); 176 | }); 177 | 178 | it("in path params", () => { 179 | useImmutable("/pet/{petId}", { 180 | params: { 181 | path: { 182 | petId: 5, 183 | // @ts-expect-error extra property should be rejected 184 | invalid_path_param: "nope", 185 | }, 186 | }, 187 | }); 188 | }); 189 | 190 | it("in header params", () => { 191 | useImmutable("/pet/findByStatus", { 192 | params: { 193 | header: { 194 | "X-Example": "test", 195 | // @ts-expect-error extra property should be rejected 196 | "Invalid-Header": "nope", 197 | }, 198 | }, 199 | }); 200 | }); 201 | }); 202 | }); 203 | 204 | describe("useInfinite", () => { 205 | it("accepts config", () => { 206 | useInfinite("/pet/findByStatus", () => null, { 207 | dedupingInterval: 10, 208 | }); 209 | }); 210 | 211 | describe("when init is required", () => { 212 | it("does not accept an empty init", () => { 213 | useInfinite( 214 | "/pet/findByTags", 215 | // @ts-expect-error empty init 216 | () => ({}), 217 | ); 218 | }); 219 | 220 | it("accepts a null init", () => { 221 | useInfinite("/pet/findByTags", () => null); 222 | }); 223 | }); 224 | 225 | describe("when init is not required", () => { 226 | it("accepts an empty init", () => { 227 | useInfinite("/pet/findByStatus", () => ({})); 228 | }); 229 | 230 | it("accepts a null init", () => { 231 | useInfinite("/pet/findByStatus", () => null); 232 | }); 233 | }); 234 | 235 | describe("rejects extra properties", () => { 236 | it("in query params", () => { 237 | useInfinite("/pet/findByStatus", () => ({ 238 | params: { 239 | query: { 240 | status: "available", 241 | // @ts-expect-error extra property should be rejected 242 | invalid_property: "nope", 243 | }, 244 | }, 245 | })); 246 | }); 247 | 248 | it("in path params", () => { 249 | useInfinite("/pet/{petId}", () => ({ 250 | params: { 251 | path: { 252 | petId: 5, 253 | // @ts-expect-error extra property should be rejected 254 | invalid_path_param: "nope", 255 | }, 256 | }, 257 | })); 258 | }); 259 | 260 | it("in header params", () => { 261 | useInfinite("/pet/findByStatus", () => ({ 262 | params: { 263 | header: { 264 | "X-Example": "test", 265 | // @ts-expect-error extra property should be rejected 266 | "Invalid-Header": "nope", 267 | }, 268 | }, 269 | })); 270 | }); 271 | }); 272 | }); 273 | 274 | describe("useMutate -> mutate", () => { 275 | it("accepts path alone", async () => { 276 | await mutate(["/pet/{petId}"]); 277 | }); 278 | 279 | it("accepts path and init", async () => { 280 | await mutate([ 281 | "/pet/{petId}", 282 | { 283 | params: { 284 | path: { 285 | petId: 5, 286 | }, 287 | }, 288 | }, 289 | ]); 290 | }); 291 | 292 | it("accepts partial init", async () => { 293 | await mutate(["/pet/{petId}", { params: {} }]); 294 | }); 295 | 296 | it("does not accept `null` init", async () => { 297 | await mutate([ 298 | "/pet/{petId}", 299 | // @ts-expect-error null not accepted 300 | null, 301 | ]); 302 | }); 303 | 304 | describe("when init is not required", () => { 305 | it("accepts path alone", async () => { 306 | await mutate(["/pet/{petId}"]); 307 | }); 308 | 309 | it("accepts path and init", async () => { 310 | await mutate([ 311 | "/pet/{petId}", 312 | { 313 | params: { 314 | path: { 315 | petId: 5, 316 | }, 317 | }, 318 | }, 319 | ]); 320 | }); 321 | 322 | it("accepts partial init", async () => { 323 | await mutate(["/pet/{petId}", { params: {} }]); 324 | }); 325 | 326 | it("does not accept `null` init", async () => { 327 | await mutate([ 328 | "/pet/{petId}", 329 | // @ts-expect-error null not accepted 330 | null, 331 | ]); 332 | }); 333 | }); 334 | 335 | describe("rejects extra properties", () => { 336 | it("in path", () => { 337 | mutate([ 338 | "/pet/{petId}", 339 | { 340 | params: { 341 | path: { 342 | petId: 5, 343 | // @ts-expect-error extra property should be rejected 344 | invalid_path_param: "no", 345 | }, 346 | }, 347 | }, 348 | ]); 349 | }); 350 | 351 | it("in query params", () => { 352 | mutate([ 353 | "/pet/findByStatus", 354 | { 355 | params: { 356 | query: { 357 | status: "available", 358 | // @ts-expect-error extra property should be rejected 359 | invalid_property: "nope", 360 | }, 361 | }, 362 | }, 363 | ]); 364 | }); 365 | 366 | it("in header params", () => { 367 | mutate([ 368 | "/pet/findByStatus", 369 | { 370 | params: { 371 | header: { 372 | "X-Example": "test", 373 | // @ts-expect-error extra property should be rejected 374 | "Invalid-Header": "nope", 375 | }, 376 | }, 377 | }, 378 | ]); 379 | }); 380 | }); 381 | }); 382 | }); 383 | 384 | describe("data types", () => { 385 | describe("useQuery", () => { 386 | it("returns correct data for path and init", () => { 387 | const { data } = useQuery("/pet/{petId}", { 388 | params: { 389 | path: { 390 | petId: 5, 391 | }, 392 | }, 393 | }); 394 | expectTypeOf(data).toEqualTypeOf(); 395 | }); 396 | 397 | it("returns correct data for path alone", () => { 398 | const { data } = useQuery("/pet/findByStatus"); 399 | expectTypeOf(data).toEqualTypeOf(); 400 | }); 401 | }); 402 | 403 | describe("useImmutable", () => { 404 | it("returns correct data", () => { 405 | const { data } = useImmutable("/pet/{petId}", { 406 | params: { 407 | path: { 408 | petId: 5, 409 | }, 410 | }, 411 | }); 412 | expectTypeOf(data).toEqualTypeOf(); 413 | }); 414 | 415 | it("returns correct data for path alone", () => { 416 | const { data } = useImmutable("/pet/findByStatus"); 417 | expectTypeOf(data).toEqualTypeOf(); 418 | }); 419 | }); 420 | 421 | describe("useInfinite", () => { 422 | it("returns correct data", () => { 423 | const { data } = useInfinite("/pet/findByStatus", (_index, _prev) => ({ 424 | params: { query: { status: "available" } }, 425 | })); 426 | 427 | expectTypeOf(data).toEqualTypeOf(); 428 | }); 429 | }); 430 | 431 | describe("useMutate -> mutate", () => { 432 | it("returns correct data", async () => { 433 | const data = await mutate(["/pet/{petId}", { params: { path: { petId: 5 } } }], { 434 | name: "Fido", 435 | photoUrls: ["https://example.com"], 436 | }); 437 | 438 | expectTypeOf(data).toEqualTypeOf>(); 439 | }); 440 | 441 | describe("when required init is not provided", () => { 442 | it("returns correct data", async () => { 443 | const data = await mutate(["/pet/{petId}"], { 444 | name: "Fido", 445 | photoUrls: ["https://example.com"], 446 | }); 447 | 448 | expectTypeOf(data).toEqualTypeOf>(); 449 | }); 450 | }); 451 | 452 | it("accepts promises in data argument", async () => { 453 | const data = Promise.resolve([{ name: "doggie", photoUrls: ["https://example.com"] }]); 454 | 455 | const result = await mutate(["/pet/findByStatus"], data); 456 | 457 | expectTypeOf(result).toEqualTypeOf<(Pet[] | undefined)[]>(); 458 | }); 459 | }); 460 | }); 461 | 462 | describe("error types", () => { 463 | describe("useQuery", () => { 464 | it("returns correct error", () => { 465 | const { error } = useQuery("/pet/{petId}", { 466 | params: { 467 | path: { 468 | petId: 5, 469 | }, 470 | }, 471 | }); 472 | 473 | expectTypeOf(error).toEqualTypeOf(); 474 | }); 475 | }); 476 | 477 | describe("useImmutable", () => { 478 | it("returns correct error", () => { 479 | const { error } = useImmutable("/pet/{petId}", { 480 | params: { 481 | path: { 482 | petId: 5, 483 | }, 484 | }, 485 | }); 486 | 487 | expectTypeOf(error).toEqualTypeOf(); 488 | }); 489 | }); 490 | 491 | describe("useInfinite", () => { 492 | it("returns correct error", () => { 493 | const { error } = useInfinite("/pet/findByStatus", (_index, _prev) => ({ 494 | params: { query: { status: "available" } }, 495 | })); 496 | 497 | expectTypeOf(error).toEqualTypeOf(); 498 | }); 499 | }); 500 | }); 501 | 502 | describe("custom error types", () => { 503 | const uniqueKey = ""; 504 | type Key = typeof uniqueKey; 505 | const useQuery = createQueryHook(client, uniqueKey); 506 | const useImmutable = createImmutableHook(client, uniqueKey); 507 | const useInfinite = createInfiniteHook(client, uniqueKey); 508 | 509 | describe("useQuery", () => { 510 | it("returns correct error", () => { 511 | const { error } = useQuery("/pet/{petId}", { 512 | params: { 513 | path: { 514 | petId: 5, 515 | }, 516 | }, 517 | }); 518 | 519 | expectTypeOf(error).toEqualTypeOf(); 520 | }); 521 | }); 522 | 523 | describe("useImmutable", () => { 524 | it("returns correct error", () => { 525 | const { error } = useImmutable("/pet/{petId}", { 526 | params: { 527 | path: { 528 | petId: 5, 529 | }, 530 | }, 531 | }); 532 | 533 | expectTypeOf(error).toEqualTypeOf(); 534 | }); 535 | }); 536 | 537 | describe("useInfinite", () => { 538 | it("returns correct error", () => { 539 | const { error } = useInfinite("/pet/findByStatus", (_index, _prev) => ({ 540 | params: { query: { status: "available" } }, 541 | })); 542 | 543 | expectTypeOf(error).toEqualTypeOf(); 544 | }); 545 | }); 546 | }); 547 | }); 548 | 549 | describe("TypesForRequest", () => { 550 | type GetPet = TypesForRequest; 551 | type FindPetsByStatus = TypesForRequest; 552 | type FindPetsByTags = TypesForRequest; 553 | 554 | describe("parity with openapi-fetch", () => { 555 | it("returns required init when params are required", () => { 556 | expectTypeOf().toMatchTypeOf<{ 557 | params: { 558 | query: { 559 | tags: string[]; 560 | }; 561 | header?: never; 562 | path?: never; 563 | cookie?: never; 564 | }; 565 | }>(); 566 | }); 567 | 568 | it("returns optional init when no params are required", () => { 569 | expectTypeOf().toMatchTypeOf< 570 | | undefined 571 | | { 572 | params: { 573 | path: { 574 | petId: number; 575 | }; 576 | query?: never; 577 | header?: never; 578 | cookie?: never; 579 | }; 580 | } 581 | >(); 582 | }); 583 | }); 584 | 585 | it("returns correct data", () => { 586 | expectTypeOf().toEqualTypeOf(); 587 | expectTypeOf().toEqualTypeOf(); 588 | expectTypeOf().toEqualTypeOf(); 589 | }); 590 | 591 | it("returns correct error", () => { 592 | expectTypeOf().toEqualTypeOf(); 593 | expectTypeOf().toEqualTypeOf(); 594 | expectTypeOf().toEqualTypeOf(); 595 | }); 596 | 597 | it("returns correct path params", () => { 598 | expectTypeOf().toEqualTypeOf<{ petId: number }>(); 599 | expectTypeOf().toEqualTypeOf(); 600 | expectTypeOf().toEqualTypeOf(); 601 | }); 602 | 603 | it("returns correct query params", () => { 604 | expectTypeOf().toEqualTypeOf(); 605 | expectTypeOf().toEqualTypeOf< 606 | | undefined 607 | | { 608 | status?: "available" | "pending" | "sold"; 609 | } 610 | >(); 611 | expectTypeOf().toEqualTypeOf<{ 612 | tags: string[]; 613 | }>(); 614 | }); 615 | 616 | it("returns correct headers", () => { 617 | expectTypeOf().toEqualTypeOf(); 618 | expectTypeOf().toEqualTypeOf< 619 | | undefined 620 | | { 621 | "X-Example": string; 622 | } 623 | >(); 624 | expectTypeOf().toEqualTypeOf(); 625 | }); 626 | 627 | it("returns correct cookies", () => { 628 | expectTypeOf().toEqualTypeOf(); 629 | expectTypeOf().toEqualTypeOf< 630 | | undefined 631 | | { 632 | "some-cookie-key": string; 633 | } 634 | >(); 635 | expectTypeOf().toEqualTypeOf(); 636 | }); 637 | 638 | it("returns correct SWR config", () => { 639 | expectTypeOf().toEqualTypeOf>(); 640 | expectTypeOf().toEqualTypeOf>(); 641 | expectTypeOf().toEqualTypeOf>(); 642 | }); 643 | 644 | it("returns correct SWR response", () => { 645 | expectTypeOf().toEqualTypeOf< 646 | SWR.SWRResponse> 647 | >(); 648 | expectTypeOf().toEqualTypeOf< 649 | SWR.SWRResponse> 650 | >(); 651 | expectTypeOf().toEqualTypeOf< 652 | SWR.SWRResponse> 653 | >(); 654 | }); 655 | }); 656 | -------------------------------------------------------------------------------- /src/__test__/fixtures/petstore.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by openapi-typescript. 3 | * Do not make direct changes to the file. 4 | */ 5 | 6 | export interface paths { 7 | "/pet": { 8 | parameters: { 9 | query?: never; 10 | header?: never; 11 | path?: never; 12 | cookie?: never; 13 | }; 14 | get?: never; 15 | /** 16 | * Update an existing pet 17 | * @description Update an existing pet by Id 18 | */ 19 | put: operations["updatePet"]; 20 | /** 21 | * Add a new pet to the store 22 | * @description Add a new pet to the store 23 | */ 24 | post: operations["addPet"]; 25 | delete?: never; 26 | options?: never; 27 | head?: never; 28 | patch?: never; 29 | trace?: never; 30 | }; 31 | "/pet/findByStatus": { 32 | parameters: { 33 | query?: never; 34 | header?: never; 35 | path?: never; 36 | cookie?: never; 37 | }; 38 | /** 39 | * Finds Pets by status 40 | * @description Multiple status values can be provided with comma separated strings 41 | */ 42 | get: operations["findPetsByStatus"]; 43 | put?: never; 44 | post?: never; 45 | delete?: never; 46 | options?: never; 47 | head?: never; 48 | patch?: never; 49 | trace?: never; 50 | }; 51 | "/pet/findByTags": { 52 | parameters: { 53 | query?: never; 54 | header?: never; 55 | path?: never; 56 | cookie?: never; 57 | }; 58 | /** 59 | * Finds Pets by tags 60 | * @description Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. 61 | */ 62 | get: operations["findPetsByTags"]; 63 | put?: never; 64 | post?: never; 65 | delete?: never; 66 | options?: never; 67 | head?: never; 68 | patch?: never; 69 | trace?: never; 70 | }; 71 | "/pet/{petId}": { 72 | parameters: { 73 | query?: never; 74 | header?: never; 75 | path?: never; 76 | cookie?: never; 77 | }; 78 | /** 79 | * Find pet by ID 80 | * @description Returns a single pet 81 | */ 82 | get: operations["getPetById"]; 83 | put?: never; 84 | /** Updates a pet in the store with form data */ 85 | post: operations["updatePetWithForm"]; 86 | /** Deletes a pet */ 87 | delete: operations["deletePet"]; 88 | options?: never; 89 | head?: never; 90 | patch?: never; 91 | trace?: never; 92 | }; 93 | "/pet/{petId}/uploadImage": { 94 | parameters: { 95 | query?: never; 96 | header?: never; 97 | path?: never; 98 | cookie?: never; 99 | }; 100 | get?: never; 101 | put?: never; 102 | /** uploads an image */ 103 | post: operations["uploadFile"]; 104 | delete?: never; 105 | options?: never; 106 | head?: never; 107 | patch?: never; 108 | trace?: never; 109 | }; 110 | "/store/inventory": { 111 | parameters: { 112 | query?: never; 113 | header?: never; 114 | path?: never; 115 | cookie?: never; 116 | }; 117 | /** 118 | * Returns pet inventories by status 119 | * @description Returns a map of status codes to quantities 120 | */ 121 | get: operations["getInventory"]; 122 | put?: never; 123 | post?: never; 124 | delete?: never; 125 | options?: never; 126 | head?: never; 127 | patch?: never; 128 | trace?: never; 129 | }; 130 | "/store/order": { 131 | parameters: { 132 | query?: never; 133 | header?: never; 134 | path?: never; 135 | cookie?: never; 136 | }; 137 | get?: never; 138 | put?: never; 139 | /** 140 | * Place an order for a pet 141 | * @description Place a new order in the store 142 | */ 143 | post: operations["placeOrder"]; 144 | delete?: never; 145 | options?: never; 146 | head?: never; 147 | patch?: never; 148 | trace?: never; 149 | }; 150 | "/store/order/{orderId}": { 151 | parameters: { 152 | query?: never; 153 | header?: never; 154 | path?: never; 155 | cookie?: never; 156 | }; 157 | /** 158 | * Find purchase order by ID 159 | * @description For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. 160 | */ 161 | get: operations["getOrderById"]; 162 | put?: never; 163 | post?: never; 164 | /** 165 | * Delete purchase order by ID 166 | * @description For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors 167 | */ 168 | delete: operations["deleteOrder"]; 169 | options?: never; 170 | head?: never; 171 | patch?: never; 172 | trace?: never; 173 | }; 174 | "/user": { 175 | parameters: { 176 | query?: never; 177 | header?: never; 178 | path?: never; 179 | cookie?: never; 180 | }; 181 | get?: never; 182 | put?: never; 183 | /** 184 | * Create user 185 | * @description This can only be done by the logged in user. 186 | */ 187 | post: operations["createUser"]; 188 | delete?: never; 189 | options?: never; 190 | head?: never; 191 | patch?: never; 192 | trace?: never; 193 | }; 194 | "/user/createWithList": { 195 | parameters: { 196 | query?: never; 197 | header?: never; 198 | path?: never; 199 | cookie?: never; 200 | }; 201 | get?: never; 202 | put?: never; 203 | /** 204 | * Creates list of users with given input array 205 | * @description Creates list of users with given input array 206 | */ 207 | post: operations["createUsersWithListInput"]; 208 | delete?: never; 209 | options?: never; 210 | head?: never; 211 | patch?: never; 212 | trace?: never; 213 | }; 214 | "/user/login": { 215 | parameters: { 216 | query?: never; 217 | header?: never; 218 | path?: never; 219 | cookie?: never; 220 | }; 221 | /** Logs user into the system */ 222 | get: operations["loginUser"]; 223 | put?: never; 224 | post?: never; 225 | delete?: never; 226 | options?: never; 227 | head?: never; 228 | patch?: never; 229 | trace?: never; 230 | }; 231 | "/user/logout": { 232 | parameters: { 233 | query?: never; 234 | header?: never; 235 | path?: never; 236 | cookie?: never; 237 | }; 238 | /** Logs out current logged in user session */ 239 | get: operations["logoutUser"]; 240 | put?: never; 241 | post?: never; 242 | delete?: never; 243 | options?: never; 244 | head?: never; 245 | patch?: never; 246 | trace?: never; 247 | }; 248 | "/user/{username}": { 249 | parameters: { 250 | query?: never; 251 | header?: never; 252 | path?: never; 253 | cookie?: never; 254 | }; 255 | /** Get user by user name */ 256 | get: operations["getUserByName"]; 257 | /** 258 | * Update user 259 | * @description This can only be done by the logged in user. 260 | */ 261 | put: operations["updateUser"]; 262 | post?: never; 263 | /** 264 | * Delete user 265 | * @description This can only be done by the logged in user. 266 | */ 267 | delete: operations["deleteUser"]; 268 | options?: never; 269 | head?: never; 270 | patch?: never; 271 | trace?: never; 272 | }; 273 | } 274 | export type webhooks = Record; 275 | export interface components { 276 | schemas: { 277 | Order: { 278 | /** 279 | * Format: int64 280 | * @example 10 281 | */ 282 | id?: number; 283 | /** 284 | * Format: int64 285 | * @example 198772 286 | */ 287 | petId?: number; 288 | /** 289 | * Format: int32 290 | * @example 7 291 | */ 292 | quantity?: number; 293 | /** Format: date-time */ 294 | shipDate?: string; 295 | /** 296 | * @description Order Status 297 | * @example approved 298 | * @enum {string} 299 | */ 300 | status?: "placed" | "approved" | "delivered"; 301 | complete?: boolean; 302 | }; 303 | Customer: { 304 | /** 305 | * Format: int64 306 | * @example 100000 307 | */ 308 | id?: number; 309 | /** @example fehguy */ 310 | username?: string; 311 | address?: components["schemas"]["Address"][]; 312 | }; 313 | Address: { 314 | /** @example 437 Lytton */ 315 | street?: string; 316 | /** @example Palo Alto */ 317 | city?: string; 318 | /** @example CA */ 319 | state?: string; 320 | /** @example 94301 */ 321 | zip?: string; 322 | }; 323 | Category: { 324 | /** 325 | * Format: int64 326 | * @example 1 327 | */ 328 | id?: number; 329 | /** @example Dogs */ 330 | name?: string; 331 | }; 332 | User: { 333 | /** 334 | * Format: int64 335 | * @example 10 336 | */ 337 | id?: number; 338 | /** @example theUser */ 339 | username?: string; 340 | /** @example John */ 341 | firstName?: string; 342 | /** @example James */ 343 | lastName?: string; 344 | /** @example john@email.com */ 345 | email?: string; 346 | /** @example 12345 */ 347 | password?: string; 348 | /** @example 12345 */ 349 | phone?: string; 350 | /** 351 | * Format: int32 352 | * @description User Status 353 | * @example 1 354 | */ 355 | userStatus?: number; 356 | }; 357 | Tag: { 358 | /** Format: int64 */ 359 | id?: number; 360 | name?: string; 361 | }; 362 | Pet: { 363 | /** 364 | * Format: int64 365 | * @example 10 366 | */ 367 | id?: number; 368 | /** @example doggie */ 369 | name: string; 370 | category?: components["schemas"]["Category"]; 371 | photoUrls: string[]; 372 | tags?: components["schemas"]["Tag"][]; 373 | /** 374 | * @description pet status in the store 375 | * @enum {string} 376 | */ 377 | status?: "available" | "pending" | "sold"; 378 | }; 379 | ApiResponse: { 380 | /** Format: int32 */ 381 | code?: number; 382 | type?: string; 383 | message?: string; 384 | }; 385 | }; 386 | responses: never; 387 | parameters: never; 388 | requestBodies: { 389 | /** @description Pet object that needs to be added to the store */ 390 | Pet: { 391 | content: { 392 | "application/json": components["schemas"]["Pet"]; 393 | "application/xml": components["schemas"]["Pet"]; 394 | }; 395 | }; 396 | /** @description List of user object */ 397 | UserArray: { 398 | content: { 399 | "application/json": components["schemas"]["User"][]; 400 | }; 401 | }; 402 | }; 403 | headers: never; 404 | pathItems: never; 405 | } 406 | export type $defs = Record; 407 | export interface operations { 408 | updatePet: { 409 | parameters: { 410 | query?: never; 411 | header?: never; 412 | path?: never; 413 | cookie?: never; 414 | }; 415 | /** @description Update an existent pet in the store */ 416 | requestBody: { 417 | content: { 418 | "application/json": components["schemas"]["Pet"]; 419 | "application/xml": components["schemas"]["Pet"]; 420 | "application/x-www-form-urlencoded": components["schemas"]["Pet"]; 421 | }; 422 | }; 423 | responses: { 424 | /** @description Successful operation */ 425 | 200: { 426 | headers: { 427 | [name: string]: unknown; 428 | }; 429 | content: { 430 | "application/xml": components["schemas"]["Pet"]; 431 | "application/json": components["schemas"]["Pet"]; 432 | }; 433 | }; 434 | /** @description Invalid ID supplied */ 435 | 400: { 436 | headers: { 437 | [name: string]: unknown; 438 | }; 439 | content?: never; 440 | }; 441 | /** @description Pet not found */ 442 | 404: { 443 | headers: { 444 | [name: string]: unknown; 445 | }; 446 | content?: never; 447 | }; 448 | /** @description Validation exception */ 449 | 405: { 450 | headers: { 451 | [name: string]: unknown; 452 | }; 453 | content?: never; 454 | }; 455 | }; 456 | }; 457 | addPet: { 458 | parameters: { 459 | query?: never; 460 | header?: never; 461 | path?: never; 462 | cookie?: never; 463 | }; 464 | /** @description Create a new pet in the store */ 465 | requestBody: { 466 | content: { 467 | "application/json": components["schemas"]["Pet"]; 468 | "application/xml": components["schemas"]["Pet"]; 469 | "application/x-www-form-urlencoded": components["schemas"]["Pet"]; 470 | }; 471 | }; 472 | responses: { 473 | /** @description Successful operation */ 474 | 200: { 475 | headers: { 476 | [name: string]: unknown; 477 | }; 478 | content: { 479 | "application/xml": components["schemas"]["Pet"]; 480 | "application/json": components["schemas"]["Pet"]; 481 | }; 482 | }; 483 | /** @description Invalid input */ 484 | 405: { 485 | headers: { 486 | [name: string]: unknown; 487 | }; 488 | content?: never; 489 | }; 490 | }; 491 | }; 492 | findPetsByStatus: { 493 | parameters: { 494 | query?: { 495 | /** @description Status values that need to be considered for filter */ 496 | status?: "available" | "pending" | "sold"; 497 | }; 498 | header?: { 499 | "X-Example": string; 500 | }; 501 | path?: never; 502 | cookie?: { 503 | "some-cookie-key": string; 504 | }; 505 | }; 506 | requestBody?: never; 507 | responses: { 508 | /** @description successful operation */ 509 | 200: { 510 | headers: { 511 | [name: string]: unknown; 512 | }; 513 | content: { 514 | "application/xml": components["schemas"]["Pet"][]; 515 | "application/json": components["schemas"]["Pet"][]; 516 | }; 517 | }; 518 | /** @description Invalid status value */ 519 | 400: { 520 | headers: { 521 | [name: string]: unknown; 522 | }; 523 | content: { 524 | "application/json": { 525 | message: "Invalid status"; 526 | }; 527 | }; 528 | }; 529 | }; 530 | }; 531 | findPetsByTags: { 532 | parameters: { 533 | query: { 534 | /** @description Tags to filter by */ 535 | tags: string[]; 536 | }; 537 | header?: never; 538 | path?: never; 539 | cookie?: never; 540 | }; 541 | requestBody?: never; 542 | responses: { 543 | /** @description successful operation */ 544 | 200: { 545 | headers: { 546 | [name: string]: unknown; 547 | }; 548 | content: { 549 | "application/xml": components["schemas"]["Pet"][]; 550 | "application/json": components["schemas"]["Pet"][]; 551 | }; 552 | }; 553 | /** @description Invalid tag value */ 554 | 400: { 555 | headers: { 556 | [name: string]: unknown; 557 | }; 558 | content?: never; 559 | }; 560 | }; 561 | }; 562 | getPetById: { 563 | parameters: { 564 | query?: never; 565 | header?: never; 566 | path: { 567 | /** @description ID of pet to return */ 568 | petId: number; 569 | }; 570 | cookie?: never; 571 | }; 572 | requestBody?: never; 573 | responses: { 574 | /** @description successful operation */ 575 | 200: { 576 | headers: { 577 | [name: string]: unknown; 578 | }; 579 | content: { 580 | "application/xml": components["schemas"]["Pet"]; 581 | "application/json": components["schemas"]["Pet"]; 582 | }; 583 | }; 584 | /** @description Invalid ID supplied */ 585 | 400: { 586 | headers: { 587 | [name: string]: unknown; 588 | }; 589 | content: { 590 | "application/json": { 591 | message: "Invalid pet configuration"; 592 | }; 593 | }; 594 | }; 595 | /** @description Pet not found */ 596 | 404: { 597 | headers: { 598 | [name: string]: unknown; 599 | }; 600 | content?: never; 601 | }; 602 | }; 603 | }; 604 | updatePetWithForm: { 605 | parameters: { 606 | query?: { 607 | /** @description Name of pet that needs to be updated */ 608 | name?: string; 609 | /** @description Status of pet that needs to be updated */ 610 | status?: string; 611 | }; 612 | header?: never; 613 | path: { 614 | /** @description ID of pet that needs to be updated */ 615 | petId: number; 616 | }; 617 | cookie?: never; 618 | }; 619 | requestBody?: never; 620 | responses: { 621 | /** @description Invalid input */ 622 | 405: { 623 | headers: { 624 | [name: string]: unknown; 625 | }; 626 | content?: never; 627 | }; 628 | }; 629 | }; 630 | deletePet: { 631 | parameters: { 632 | query?: never; 633 | header?: { 634 | api_key?: string; 635 | }; 636 | path: { 637 | /** @description Pet id to delete */ 638 | petId: number; 639 | }; 640 | cookie?: never; 641 | }; 642 | requestBody?: never; 643 | responses: { 644 | /** @description Invalid pet value */ 645 | 400: { 646 | headers: { 647 | [name: string]: unknown; 648 | }; 649 | content?: never; 650 | }; 651 | }; 652 | }; 653 | uploadFile: { 654 | parameters: { 655 | query?: { 656 | /** @description Additional Metadata */ 657 | additionalMetadata?: string; 658 | }; 659 | header?: never; 660 | path: { 661 | /** @description ID of pet to update */ 662 | petId: number; 663 | }; 664 | cookie?: never; 665 | }; 666 | requestBody?: { 667 | content: { 668 | "application/octet-stream": string; 669 | }; 670 | }; 671 | responses: { 672 | /** @description successful operation */ 673 | 200: { 674 | headers: { 675 | [name: string]: unknown; 676 | }; 677 | content: { 678 | "application/json": components["schemas"]["ApiResponse"]; 679 | }; 680 | }; 681 | }; 682 | }; 683 | getInventory: { 684 | parameters: { 685 | query?: never; 686 | header?: never; 687 | path?: never; 688 | cookie?: never; 689 | }; 690 | requestBody?: never; 691 | responses: { 692 | /** @description successful operation */ 693 | 200: { 694 | headers: { 695 | [name: string]: unknown; 696 | }; 697 | content: { 698 | "application/json": { 699 | [key: string]: number; 700 | }; 701 | }; 702 | }; 703 | }; 704 | }; 705 | placeOrder: { 706 | parameters: { 707 | query?: never; 708 | header?: never; 709 | path?: never; 710 | cookie?: never; 711 | }; 712 | requestBody?: { 713 | content: { 714 | "application/json": components["schemas"]["Order"]; 715 | "application/xml": components["schemas"]["Order"]; 716 | "application/x-www-form-urlencoded": components["schemas"]["Order"]; 717 | }; 718 | }; 719 | responses: { 720 | /** @description successful operation */ 721 | 200: { 722 | headers: { 723 | [name: string]: unknown; 724 | }; 725 | content: { 726 | "application/json": components["schemas"]["Order"]; 727 | }; 728 | }; 729 | /** @description Invalid input */ 730 | 405: { 731 | headers: { 732 | [name: string]: unknown; 733 | }; 734 | content?: never; 735 | }; 736 | }; 737 | }; 738 | getOrderById: { 739 | parameters: { 740 | query?: never; 741 | header?: never; 742 | path: { 743 | /** @description ID of order that needs to be fetched */ 744 | orderId: number; 745 | }; 746 | cookie?: never; 747 | }; 748 | requestBody?: never; 749 | responses: { 750 | /** @description successful operation */ 751 | 200: { 752 | headers: { 753 | [name: string]: unknown; 754 | }; 755 | content: { 756 | "application/xml": components["schemas"]["Order"]; 757 | "application/json": components["schemas"]["Order"]; 758 | }; 759 | }; 760 | /** @description Invalid ID supplied */ 761 | 400: { 762 | headers: { 763 | [name: string]: unknown; 764 | }; 765 | content?: never; 766 | }; 767 | /** @description Order not found */ 768 | 404: { 769 | headers: { 770 | [name: string]: unknown; 771 | }; 772 | content?: never; 773 | }; 774 | }; 775 | }; 776 | deleteOrder: { 777 | parameters: { 778 | query?: never; 779 | header?: never; 780 | path: { 781 | /** @description ID of the order that needs to be deleted */ 782 | orderId: number; 783 | }; 784 | cookie?: never; 785 | }; 786 | requestBody?: never; 787 | responses: { 788 | /** @description Invalid ID supplied */ 789 | 400: { 790 | headers: { 791 | [name: string]: unknown; 792 | }; 793 | content?: never; 794 | }; 795 | /** @description Order not found */ 796 | 404: { 797 | headers: { 798 | [name: string]: unknown; 799 | }; 800 | content?: never; 801 | }; 802 | }; 803 | }; 804 | createUser: { 805 | parameters: { 806 | query?: never; 807 | header?: never; 808 | path?: never; 809 | cookie?: never; 810 | }; 811 | /** @description Created user object */ 812 | requestBody?: { 813 | content: { 814 | "application/json": components["schemas"]["User"]; 815 | "application/xml": components["schemas"]["User"]; 816 | "application/x-www-form-urlencoded": components["schemas"]["User"]; 817 | }; 818 | }; 819 | responses: { 820 | /** @description successful operation */ 821 | default: { 822 | headers: { 823 | [name: string]: unknown; 824 | }; 825 | content: { 826 | "application/json": components["schemas"]["User"]; 827 | "application/xml": components["schemas"]["User"]; 828 | }; 829 | }; 830 | }; 831 | }; 832 | createUsersWithListInput: { 833 | parameters: { 834 | query?: never; 835 | header?: never; 836 | path?: never; 837 | cookie?: never; 838 | }; 839 | requestBody?: { 840 | content: { 841 | "application/json": components["schemas"]["User"][]; 842 | }; 843 | }; 844 | responses: { 845 | /** @description Successful operation */ 846 | 200: { 847 | headers: { 848 | [name: string]: unknown; 849 | }; 850 | content: { 851 | "application/xml": components["schemas"]["User"]; 852 | "application/json": components["schemas"]["User"]; 853 | }; 854 | }; 855 | /** @description successful operation */ 856 | default: { 857 | headers: { 858 | [name: string]: unknown; 859 | }; 860 | content?: never; 861 | }; 862 | }; 863 | }; 864 | loginUser: { 865 | parameters: { 866 | query?: { 867 | /** @description The user name for login */ 868 | username?: string; 869 | /** @description The password for login in clear text */ 870 | password?: string; 871 | }; 872 | header?: never; 873 | path?: never; 874 | cookie?: never; 875 | }; 876 | requestBody?: never; 877 | responses: { 878 | /** @description successful operation */ 879 | 200: { 880 | headers: { 881 | /** @description calls per hour allowed by the user */ 882 | "X-Rate-Limit"?: number; 883 | /** @description date in UTC when token expires */ 884 | "X-Expires-After"?: string; 885 | [name: string]: unknown; 886 | }; 887 | content: { 888 | "application/xml": string; 889 | "application/json": string; 890 | }; 891 | }; 892 | /** @description Invalid username/password supplied */ 893 | 400: { 894 | headers: { 895 | [name: string]: unknown; 896 | }; 897 | content?: never; 898 | }; 899 | }; 900 | }; 901 | logoutUser: { 902 | parameters: { 903 | query?: never; 904 | header?: never; 905 | path?: never; 906 | cookie?: never; 907 | }; 908 | requestBody?: never; 909 | responses: { 910 | /** @description successful operation */ 911 | default: { 912 | headers: { 913 | [name: string]: unknown; 914 | }; 915 | content?: never; 916 | }; 917 | }; 918 | }; 919 | getUserByName: { 920 | parameters: { 921 | query?: never; 922 | header?: never; 923 | path: { 924 | /** @description The name that needs to be fetched. Use user1 for testing. */ 925 | username: string; 926 | }; 927 | cookie?: never; 928 | }; 929 | requestBody?: never; 930 | responses: { 931 | /** @description successful operation */ 932 | 200: { 933 | headers: { 934 | [name: string]: unknown; 935 | }; 936 | content: { 937 | "application/xml": components["schemas"]["User"]; 938 | "application/json": components["schemas"]["User"]; 939 | }; 940 | }; 941 | /** @description Invalid username supplied */ 942 | 400: { 943 | headers: { 944 | [name: string]: unknown; 945 | }; 946 | content?: never; 947 | }; 948 | /** @description User not found */ 949 | 404: { 950 | headers: { 951 | [name: string]: unknown; 952 | }; 953 | content?: never; 954 | }; 955 | }; 956 | }; 957 | updateUser: { 958 | parameters: { 959 | query?: never; 960 | header?: never; 961 | path: { 962 | /** @description name that needs to be updated */ 963 | username: string; 964 | }; 965 | cookie?: never; 966 | }; 967 | /** @description Update an existent user in the store */ 968 | requestBody?: { 969 | content: { 970 | "application/json": components["schemas"]["User"]; 971 | "application/xml": components["schemas"]["User"]; 972 | "application/x-www-form-urlencoded": components["schemas"]["User"]; 973 | }; 974 | }; 975 | responses: { 976 | /** @description successful operation */ 977 | default: { 978 | headers: { 979 | [name: string]: unknown; 980 | }; 981 | content?: never; 982 | }; 983 | }; 984 | }; 985 | deleteUser: { 986 | parameters: { 987 | query?: never; 988 | header?: never; 989 | path: { 990 | /** @description The name that needs to be deleted */ 991 | username: string; 992 | }; 993 | cookie?: never; 994 | }; 995 | requestBody?: never; 996 | responses: { 997 | /** @description Invalid username supplied */ 998 | 400: { 999 | headers: { 1000 | [name: string]: unknown; 1001 | }; 1002 | content?: never; 1003 | }; 1004 | /** @description User not found */ 1005 | 404: { 1006 | headers: { 1007 | [name: string]: unknown; 1008 | }; 1009 | content?: never; 1010 | }; 1011 | }; 1012 | }; 1013 | } 1014 | --------------------------------------------------------------------------------