├── .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 |
10 |
11 |
12 |
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 |
--------------------------------------------------------------------------------